175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
import asyncio
|
|
import functools
|
|
import re
|
|
import tempfile
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any, Callable, ParamSpec, TypeVar
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.skip(
|
|
reason="Disabled by #21985: dev installs use git+ which doesn't respect UV_EXCLUDE_NEWER"
|
|
)
|
|
|
|
from flavors._matrix import generate_matrix
|
|
|
|
_P = ParamSpec("_P")
|
|
_R = TypeVar("_R")
|
|
|
|
|
|
class MockResponse:
|
|
def __init__(self, data: dict[str, Any]) -> None:
|
|
self.data = data
|
|
|
|
def json(self) -> dict[str, Any]:
|
|
return self.data
|
|
|
|
def raise_for_status(self) -> None:
|
|
pass
|
|
|
|
@classmethod
|
|
def from_versions(cls, versions: list[str]) -> "MockResponse":
|
|
return cls({
|
|
"releases": {
|
|
v: [
|
|
{
|
|
"filename": v + ".whl",
|
|
"upload_time": "2023-10-04T16:38:57",
|
|
}
|
|
]
|
|
for v in versions
|
|
}
|
|
})
|
|
|
|
|
|
def mock_pypi_api(
|
|
mock_responses: dict[str, MockResponse],
|
|
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
|
def requests_get_patch(url: str, *args: Any, **kwargs: Any) -> MockResponse:
|
|
match = re.search(r"https://pypi\.org/pypi/(.+)/json", url)
|
|
assert match is not None
|
|
return mock_responses[match.group(1)]
|
|
|
|
def decorator(test_func: Callable[_P, _R]) -> Callable[_P, _R]:
|
|
@functools.wraps(test_func)
|
|
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
with mock.patch("requests.get", new=requests_get_patch):
|
|
return test_func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
@contextmanager
|
|
def mock_ml_package_versions_yml(src_base: str, src_ref: str) -> Iterator[list[str]]:
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
yml_base = Path(tmp_dir).joinpath("base.yml")
|
|
yml_ref = Path(tmp_dir).joinpath("ref.yml")
|
|
yml_base.write_text(src_base)
|
|
yml_ref.write_text(src_ref)
|
|
yield ["--versions-yaml", str(yml_base), "--ref-versions-yaml", str(yml_ref)]
|
|
|
|
|
|
MOCK_YAML_SOURCE = """
|
|
foo:
|
|
package_info:
|
|
pip_release: foo
|
|
install_dev: "pip install git+https://github.com/foo/foo.git"
|
|
|
|
autologging:
|
|
minimum: "1.0.0"
|
|
maximum: "1.2.0"
|
|
run: pytest tests/foo
|
|
|
|
bar:
|
|
package_info:
|
|
pip_release: bar
|
|
install_dev: "pip install git+https://github.com/bar/bar.git"
|
|
|
|
autologging:
|
|
minimum: "1.3"
|
|
maximum: "1.4"
|
|
run: pytest/tests bar
|
|
"""
|
|
|
|
MOCK_PYPI_API_RESPONSES = {
|
|
"foo": MockResponse.from_versions(["1.0.0", "1.1.0", "1.1.1", "1.2.0"]),
|
|
"bar": MockResponse.from_versions(["1.3", "1.4"]),
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("flavors", "expected"),
|
|
[
|
|
("foo", {"foo"}),
|
|
("foo,bar", {"foo", "bar"}),
|
|
("foo, bar", {"foo", "bar"}), # Contains a space after a comma
|
|
("", {"foo", "bar"}),
|
|
(None, {"foo", "bar"}),
|
|
],
|
|
)
|
|
@mock_pypi_api(MOCK_PYPI_API_RESPONSES)
|
|
def test_flavors(flavors: str | None, expected: set[str]) -> None:
|
|
with mock_ml_package_versions_yml(MOCK_YAML_SOURCE, "{}") as path_args:
|
|
flavors_args = [] if flavors is None else ["--flavors", flavors]
|
|
matrix = asyncio.run(generate_matrix([*path_args, *flavors_args]))
|
|
flavor_names = {x.flavor for x in matrix}
|
|
assert flavor_names == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("versions", "expected"),
|
|
[
|
|
("1.0.0", {"1.0.0"}),
|
|
("1.0.0,1.1.1", {"1.0.0", "1.1.1"}),
|
|
("1.3, 1.4", {"1.3", "1.4"}), # Contains a space after a comma
|
|
("", {"1.0.0", "1.1.1", "1.2.0", "1.3", "1.4", "dev"}),
|
|
(None, {"1.0.0", "1.1.1", "1.2.0", "1.3", "1.4", "dev"}),
|
|
],
|
|
)
|
|
@mock_pypi_api(MOCK_PYPI_API_RESPONSES)
|
|
def test_versions(versions: str | None, expected: set[str]) -> None:
|
|
with mock_ml_package_versions_yml(MOCK_YAML_SOURCE, "{}") as path_args:
|
|
versions_args = [] if versions is None else ["--versions", versions]
|
|
matrix = asyncio.run(generate_matrix([*path_args, *versions_args]))
|
|
version_strs = {str(x.version) for x in matrix}
|
|
assert version_strs == expected
|
|
|
|
|
|
@mock_pypi_api(MOCK_PYPI_API_RESPONSES)
|
|
def test_flavors_and_versions() -> None:
|
|
with mock_ml_package_versions_yml(MOCK_YAML_SOURCE, "{}") as path_args:
|
|
matrix = asyncio.run(
|
|
generate_matrix([*path_args, "--flavors", "foo,bar", "--versions", "dev"])
|
|
)
|
|
flavors = {x.flavor for x in matrix}
|
|
versions = {str(x.version) for x in matrix}
|
|
assert flavors == {"foo", "bar"}
|
|
assert versions == {"dev"}
|
|
|
|
|
|
@mock_pypi_api(MOCK_PYPI_API_RESPONSES)
|
|
def test_no_dev() -> None:
|
|
with mock_ml_package_versions_yml(MOCK_YAML_SOURCE, "{}") as path_args:
|
|
matrix = asyncio.run(generate_matrix([*path_args, "--no-dev"]))
|
|
flavors = {x.flavor for x in matrix}
|
|
versions = {str(x.version) for x in matrix}
|
|
assert flavors == {"foo", "bar"}
|
|
assert versions == {"1.0.0", "1.1.1", "1.2.0", "1.3", "1.4"}
|
|
|
|
|
|
@mock_pypi_api(MOCK_PYPI_API_RESPONSES)
|
|
def test_changed_files() -> None:
|
|
with mock_ml_package_versions_yml(MOCK_YAML_SOURCE, MOCK_YAML_SOURCE) as path_args:
|
|
matrix = asyncio.run(
|
|
generate_matrix([*path_args, "--changed-files", "mlflow/foo/__init__.py"])
|
|
)
|
|
flavors = {x.flavor for x in matrix}
|
|
versions = {str(x.version) for x in matrix}
|
|
assert flavors == {"foo"}
|
|
assert versions == {"1.0.0", "1.1.1", "1.2.0", "dev"}
|