ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
143 lines
5.3 KiB
Python
143 lines
5.3 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Guard tests for the release-cut dependency contract.
|
|
|
|
These tests pin the public dependency surface so that release-blocking
|
|
regressions documented in the bare-install audit cannot silently re-emerge:
|
|
|
|
* ``packaging`` MUST be declared in main deps (used at import-time by
|
|
``utils/model_name_utils.py`` and ``cli/cli_deploy.py``; reachable from
|
|
``from google.adk import Runner`` and from ``adk --help``).
|
|
* ``ValidationError`` in ``environment_simulation_config`` MUST come from
|
|
``pydantic`` (which always installs alongside the package), NOT from the
|
|
undeclared ``pydantic_core``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import tomllib
|
|
except ImportError:
|
|
import tomli as tomllib
|
|
|
|
import pytest
|
|
|
|
|
|
def _find_pyproject() -> Path:
|
|
"""Locates pyproject.toml by walking up from this file's directory.
|
|
|
|
Handles layouts where pyproject.toml is at an ancestor directory as well as
|
|
layouts where it lives in a sibling build directory next to the package. The
|
|
test tree may be symlinked, so the walk avoids ``.resolve()``.
|
|
"""
|
|
start = Path(__file__).parent
|
|
for candidate in [start, *start.parents]:
|
|
direct = candidate / 'pyproject.toml'
|
|
if direct.is_file():
|
|
return direct
|
|
try:
|
|
children = sorted(p for p in candidate.iterdir() if p.is_dir())
|
|
except OSError:
|
|
continue
|
|
for child in children:
|
|
sibling = child / 'pyproject.toml'
|
|
if sibling.is_file():
|
|
return sibling
|
|
raise FileNotFoundError(
|
|
f'Could not find pyproject.toml walking up from {start}.'
|
|
)
|
|
|
|
|
|
_PYPROJECT_PATH = _find_pyproject()
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
def pyproject() -> dict:
|
|
"""Parses the project's pyproject.toml exactly once for the module."""
|
|
with _PYPROJECT_PATH.open('rb') as fh:
|
|
return tomllib.load(fh)
|
|
|
|
|
|
def _requirement_names(requirements: list[str]) -> set[str]:
|
|
"""Returns the lowercased PEP 508 distribution names from ``requirements``.
|
|
|
|
Strips extras specifiers, version specifiers, and environment markers so the
|
|
caller can do exact-name membership checks.
|
|
"""
|
|
names: set[str] = set()
|
|
for req in requirements:
|
|
# Drop everything after a marker, version specifier, or extras block.
|
|
head = req.split(';', 1)[0].strip()
|
|
for sep in ('[', '>', '<', '=', '!', '~', ' '):
|
|
head = head.split(sep, 1)[0]
|
|
names.add(head.strip().lower())
|
|
return names
|
|
|
|
|
|
def test_main_deps_include_packaging(pyproject: dict) -> None:
|
|
"""``packaging`` is imported unguarded by core ADK; it must be a main dep."""
|
|
main_deps = _requirement_names(pyproject['project']['dependencies'])
|
|
assert 'packaging' in main_deps, (
|
|
'packaging must be declared in [project] dependencies because '
|
|
'src/google/adk/utils/model_name_utils.py and '
|
|
'src/google/adk/cli/cli_deploy.py import it unguarded at module top '
|
|
'level. Without this declaration, `pip install google-adk` is one '
|
|
'transitive resolver change away from breaking on `import google.adk`.'
|
|
)
|
|
|
|
|
|
def test_environment_simulation_config_imports_validation_error_from_pydantic() -> (
|
|
None
|
|
):
|
|
"""The ValidationError used by the config module must come from pydantic.
|
|
|
|
pydantic-core is undeclared; importing from it directly is fragile. pydantic
|
|
re-exports ValidationError, so use that.
|
|
"""
|
|
# Use importlib to locate the source file so the test is independent of the
|
|
# on-disk package layout.
|
|
spec = importlib.util.find_spec(
|
|
'google.adk.tools.environment_simulation.environment_simulation_config'
|
|
)
|
|
assert (
|
|
spec is not None and spec.origin is not None
|
|
), 'environment_simulation_config module is not importable.'
|
|
source_path = Path(spec.origin)
|
|
source = source_path.read_text(encoding='utf-8')
|
|
assert 'from pydantic import ValidationError' in source, (
|
|
'environment_simulation_config.py must import ValidationError from '
|
|
'pydantic, not pydantic_core. pydantic_core is undeclared as a main '
|
|
'dep and pydantic re-exports the same class.'
|
|
)
|
|
assert 'from pydantic_core import ValidationError' not in source, (
|
|
'environment_simulation_config.py must not import ValidationError '
|
|
'from pydantic_core (undeclared dep).'
|
|
)
|
|
|
|
|
|
def test_injection_config_validation_raises_pydantic_validation_error() -> None:
|
|
"""Behavioral check: invalid config raises the pydantic ValidationError."""
|
|
# Local import keeps this test focused on the post-fix code path and
|
|
# surfaces ImportError clearly if the module's import block regresses.
|
|
from google.adk.tools.environment_simulation.environment_simulation_config import InjectedError
|
|
from pydantic import ValidationError
|
|
|
|
with pytest.raises(ValidationError):
|
|
# Both required fields missing — pydantic must reject the construction.
|
|
InjectedError() # type: ignore[call-arg]
|