diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..cc3ec90 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +from hotdata_runtime import QueryResult + + +@pytest.fixture +def sample_result() -> QueryResult: + return QueryResult( + columns=["n"], + rows=[[1]], + row_count=1, + result_id="res_1", + query_run_id="run_1", + execution_time_ms=10, + warning=None, + error_message=None, + ) + + +@pytest.fixture +def mock_client(sample_result: QueryResult): + client = MagicMock() + client.workspace_id = "ws_test" + client.host = "https://api.hotdata.dev" + client.session_id = "sb_1" + client.execute_sql = MagicMock(return_value=sample_result) + client.connections.return_value.list_connections.return_value = SimpleNamespace( + connections=[] + ) + return client diff --git a/tests/test_architecture_guardrails.py b/tests/test_architecture_guardrails.py deleted file mode 100644 index 8b5ee3b..0000000 --- a/tests/test_architecture_guardrails.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import re -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE_ROOT = REPO_ROOT / "hotdata_marimo" - - -def test_source_uses_hotdata_runtime_root_imports() -> None: - violations: list[str] = [] - pattern = re.compile( - r"(?m)^\s*(?:from\s+hotdata_runtime\.(client|env|result|health)\s+import" - r"|import\s+hotdata_runtime\.(client|env|result|health)(?:\s|$|,|as))" - ) - - for path in SOURCE_ROOT.rglob("*.py"): - text = path.read_text(encoding="utf-8") - if pattern.search(text): - violations.append(str(path.relative_to(REPO_ROOT))) - - assert not violations, ( - "Use `from hotdata_runtime import ...` in package source; " - f"found submodule imports in: {', '.join(violations)}" - ) diff --git a/tests/test_hotdata_marimo.py b/tests/test_hotdata_marimo.py new file mode 100644 index 0000000..377f249 --- /dev/null +++ b/tests/test_hotdata_marimo.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +import hotdata_marimo as hm +from hotdata_runtime import HotdataClient +from hotdata_marimo.display import _option_map_with_unique_labels +from hotdata_marimo.sql_engine import HotdataMarimoEngine +from hotdata_marimo.table_browser import _connection_options +from hotdata_marimo.workspace_selector import WorkspaceSelector, workspace_selector_from_env +from marimo._types.ids import VariableName + + +def _selection(*, workspace_id: str, source: str, workspaces: list | None = None): + return SimpleNamespace( + workspace_id=workspace_id, + source=source, + workspaces=workspaces or [], + ) + + +def _workspace_row(name: str, public_id: str, *, active: bool = True): + return SimpleNamespace(name=name, public_id=public_id, active=active) + + +@pytest.mark.parametrize( + ("labels", "expected"), + [ + ( + [("dup", "a"), ("dup", "b"), ("dup", "c")], + {"dup": "a", "dup (2)": "b", "dup (3)": "c"}, + ), + ( + [], + {}, + ), + ], +) +def test_option_map_with_unique_labels(labels, expected): + assert _option_map_with_unique_labels(labels) == expected + + +def test_connection_options_disambiguates_duplicate_names(): + conns = [ + SimpleNamespace(name="Warehouse", id="conn_1"), + SimpleNamespace(name="Warehouse", id="conn_2"), + SimpleNamespace(name="Analytics", id="conn_3"), + ] + assert _connection_options(conns) == { + "Warehouse": "conn_1", + "Warehouse (conn_2)": "conn_2", + "Analytics": "conn_3", + } + + +@pytest.mark.parametrize( + ("resolve", "expect_dropdown", "expected_workspace"), + [ + ( + _selection(workspace_id="ws_explicit", source="explicit_env"), + False, + "ws_explicit", + ), + ( + _selection( + workspace_id="ws_only", + source="active", + workspaces=[_workspace_row("Only", "ws_only")], + ), + False, + "ws_only", + ), + ( + _selection( + workspace_id="ws_a", + source="active", + workspaces=[ + _workspace_row("Alpha", "ws_a"), + _workspace_row("Beta", "ws_b", active=False), + ], + ), + True, + "ws_b", + ), + ], +) +def test_workspace_selector(resolve, expect_dropdown, expected_workspace): + pick = MagicMock() + pick.value = resolve.workspace_id + with patch( + "hotdata_marimo.workspace_selector.resolve_workspace_selection", + return_value=resolve, + ), patch( + "hotdata_marimo.workspace_selector.mo.ui.dropdown", + return_value=pick, + ): + selector = WorkspaceSelector(api_key="k") + if expect_dropdown: + pick.value = expected_workspace + assert (selector._pick is not None) is expect_dropdown + assert selector.workspace_id == expected_workspace + assert selector.client.workspace_id == expected_workspace + + +def test_workspace_selector_from_env_requires_api_key(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("HOTDATA_API_KEY", raising=False) + with pytest.raises(RuntimeError, match="HOTDATA_API_KEY"): + workspace_selector_from_env() + + +def test_register_hotdata_sql_engine_is_idempotent() -> None: + from marimo._sql.get_engines import SUPPORTED_ENGINES + + hm.unregister_hotdata_sql_engine() + assert SUPPORTED_ENGINES.count(HotdataMarimoEngine) == 0 + try: + hm.register_hotdata_sql_engine() + hm.register_hotdata_sql_engine() + assert SUPPORTED_ENGINES.count(HotdataMarimoEngine) == 1 + finally: + hm.unregister_hotdata_sql_engine() + + +def test_hotdata_engine_display_name_in_marimo_ui(mock_client) -> None: + hm.register_hotdata_sql_engine() + try: + engine = HotdataMarimoEngine(mock_client, engine_name=VariableName("client")) + import marimo._sql.get_engines as ge + import marimo._runtime.runner.hooks_post_execution as hpe + + for module in (ge, hpe): + conn = module.engine_to_data_source_connection(VariableName("client"), engine) + assert conn.display_name == "Hotdata" + finally: + hm.unregister_hotdata_sql_engine() diff --git a/tests/test_imports.py b/tests/test_imports.py deleted file mode 100644 index 041a3b8..0000000 --- a/tests/test_imports.py +++ /dev/null @@ -1,7 +0,0 @@ -def test_package_imports(): - import hotdata_marimo as hm - - assert hm.HotdataClient is not None - assert hm.SqlEditor is not None - assert hm.register_hotdata_sql_engine is not None - assert hm.HotdataMarimoEngine is not None diff --git a/tests/test_options.py b/tests/test_options.py deleted file mode 100644 index 38d975d..0000000 --- a/tests/test_options.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from types import SimpleNamespace - -from hotdata_marimo.display import _option_map_with_unique_labels -from hotdata_marimo.table_browser import _connection_options - - -def test_option_map_with_unique_labels_keeps_all_values(): - options = _option_map_with_unique_labels( - [("dup", "a"), ("dup", "b"), ("dup", "c")] - ) - assert options == { - "dup": "a", - "dup (2)": "b", - "dup (3)": "c", - } - - -def test_connection_options_disambiguates_duplicate_names(): - conns = [ - SimpleNamespace(name="Warehouse", id="conn_1"), - SimpleNamespace(name="Warehouse", id="conn_2"), - SimpleNamespace(name="Analytics", id="conn_3"), - ] - options = _connection_options(conns) - assert options == { - "Warehouse": "conn_1", - "Warehouse (conn_2)": "conn_2", - "Analytics": "conn_3", - } diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..3ffa0fb --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import importlib +import re +from pathlib import Path + +import pytest +from importlib.metadata import version as dist_version + +import hotdata_marimo as hm + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = REPO_ROOT / "hotdata_marimo" +_RUNTIME_SUBMODULE = re.compile( + r"(?m)^\s*(?:from\s+hotdata_runtime\.(client|env|result|health)\s+import" + r"|import\s+hotdata_runtime\.(client|env|result|health)(?:\s|$|,|as))" +) + + +def test_version_is_pep440_core(): + assert re.fullmatch(r"\d+\.\d+\.\d+(\+.*)?", hm.__version__) + + +def test_version_matches_distribution_metadata(): + assert dist_version("hotdata-marimo") == hm.__version__ + + +@pytest.mark.parametrize("name", hm.__all__) +def test_public_export_is_importable(name: str): + assert hasattr(hm, name), f"missing export: {name}" + assert getattr(hm, name) is not None + + +def test_runtime_primitives_are_reexported(): + from hotdata_runtime import HotdataClient, QueryResult, from_env + + assert hm.HotdataClient is HotdataClient + assert hm.QueryResult is QueryResult + assert hm.from_env is from_env + + +@pytest.mark.parametrize( + ("alias", "target"), + [ + ("hotdata_sql_editor", "sql_editor"), + ("hotdata_table_browser", "table_browser"), + ("hotdata_query_result", "query_result"), + ("hotdata_connection_picker", "connection_picker"), + ("hotdata_workspace_selector", "workspace_selector_from_env"), + ("hotdata_recent_results", "recent_results"), + ], +) +def test_mo_ui_aliases_match_public_helpers(alias: str, target: str): + assert getattr(hm, alias) is getattr(hm, target) + + +def test_source_uses_hotdata_runtime_root_imports(): + violations: list[str] = [] + for path in SOURCE_ROOT.rglob("*.py"): + if _RUNTIME_SUBMODULE.search(path.read_text(encoding="utf-8")): + violations.append(str(path.relative_to(REPO_ROOT))) + assert not violations, ( + "Use `from hotdata_runtime import ...` in package source; " + f"found submodule imports in: {', '.join(violations)}" + ) + + +def test_no_stale_submodule_surface(): + with pytest.raises(ModuleNotFoundError): + importlib.import_module("hotdata_marimo.client") diff --git a/tests/test_sql_engine_registry.py b/tests/test_sql_engine_registry.py deleted file mode 100644 index b836572..0000000 --- a/tests/test_sql_engine_registry.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -from types import SimpleNamespace -from unittest.mock import MagicMock - -import hotdata_marimo as hm -from hotdata_runtime import HotdataClient -from hotdata_marimo.sql_engine import HotdataMarimoEngine -from marimo._types.ids import VariableName - - -def test_register_hotdata_sql_engine_is_idempotent() -> None: - from marimo._sql.get_engines import SUPPORTED_ENGINES - - hm.unregister_hotdata_sql_engine() - assert SUPPORTED_ENGINES.count(HotdataMarimoEngine) == 0 - try: - hm.register_hotdata_sql_engine() - hm.register_hotdata_sql_engine() - assert SUPPORTED_ENGINES.count(HotdataMarimoEngine) == 1 - finally: - hm.unregister_hotdata_sql_engine() - - -def test_hotdata_engine_display_name_in_marimo_ui() -> None: - hm.register_hotdata_sql_engine() - try: - client = MagicMock(spec=HotdataClient) - client.connections.return_value.list_connections.return_value = ( - SimpleNamespace(connections=[]) - ) - engine = HotdataMarimoEngine(client, engine_name=VariableName("client")) - import marimo._sql.get_engines as ge - - conn = ge.engine_to_data_source_connection(VariableName("client"), engine) - assert conn.display_name == "Hotdata" - - import marimo._runtime.runner.hooks_post_execution as hpe - - conn_hpe = hpe.engine_to_data_source_connection( - VariableName("client"), engine - ) - assert conn_hpe.display_name == "Hotdata" - finally: - hm.unregister_hotdata_sql_engine() diff --git a/tests/test_version.py b/tests/test_version.py deleted file mode 100644 index 2a1d019..0000000 --- a/tests/test_version.py +++ /dev/null @@ -1,13 +0,0 @@ -import re - -from importlib.metadata import version as dist_version - -import hotdata_marimo as hm - - -def test_version_is_pep440_core(): - assert re.fullmatch(r"\d+\.\d+\.\d+(\+.*)?", hm.__version__) - - -def test_version_matches_distribution_metadata(): - assert dist_version("hotdata-marimo") == hm.__version__