Files
mlflow--mlflow/tests/store/test_workspace_model_coverage.py
2026-07-13 13:22:34 +08:00

83 lines
3.2 KiB
Python

import ast
from pathlib import Path
# Explicitly import all dbmodel modules so that every ORM model is registered
# with Base.registry before we inspect mappers.
import mlflow.store.model_registry.dbmodels.models # noqa: F401
import mlflow.store.tracking.dbmodels.models # noqa: F401
import mlflow.store.workspace.dbmodels.models # noqa: F401
from mlflow.store.db.base_sql_model import Base
# Locate the repository root relative to this test file so that workspace
# store paths resolve correctly regardless of the pytest working directory.
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
# Every store that provides a ``_get_query`` with workspace filtering.
WORKSPACE_STORE_PATHS = [
"mlflow/store/tracking/sqlalchemy_workspace_store.py",
"mlflow/store/model_registry/sqlalchemy_store.py",
"mlflow/store/jobs/sqlalchemy_workspace_store.py",
]
def _models_handled_by_get_query(ws_path: str) -> set[str]:
"""Parse a workspace store file and return the ``Sql*`` model names
referenced in comparisons inside its ``_get_query`` method.
"""
tree = ast.parse((_REPO_ROOT / ws_path).read_text())
# Collect module-level variables holding Sql* names (e.g. tuples of models)
var_models: dict[str, set[str]] = {}
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
names = {
n.id
for n in ast.walk(node.value)
if isinstance(n, ast.Name) and n.id.startswith("Sql")
}
if names:
var_models[target.id] = names
# Extract model names from _get_query comparisons
models: set[str] = set()
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if node.name != "_get_query":
continue
for inner in ast.walk(node):
if not isinstance(inner, ast.Compare):
continue
for comp in inner.comparators:
models |= {
n.id
for n in ast.walk(comp)
if isinstance(n, ast.Name) and n.id.startswith("Sql")
}
if isinstance(comp, ast.Name) and comp.id in var_models:
models |= var_models[comp.id]
return models
def test_all_workspace_models_handled_in_get_query():
"""Every model with a workspace column must appear in at least one
workspace store's ``_get_query``.
"""
handled: set[str] = set()
for ws_path in WORKSPACE_STORE_PATHS:
handled |= _models_handled_by_get_query(ws_path)
models_with_column: set[str] = set()
for mapper in Base.registry.mappers:
if "workspace" in {col.key for col in mapper.columns}:
models_with_column.add(mapper.class_.__name__)
missing = models_with_column - handled
assert not missing, (
f"These models have a `workspace` column but are not handled by any "
f"workspace store's _get_query: {sorted(missing)}. "
f"Add handling in the appropriate workspace store's _get_query method."
)