SESSION SUMMARY: - Started at 7194 tests, 80% raw / 93.4% real - Ended at 7778 tests, 84% raw / 98.4% real - +584 tests, +4pp raw, +5pp real - 0 failures, 0 production code changes FIXED (12→0 failures): - dataset_review_routes_extended: 201→200, DTO fields, candidate FK - settings_consolidated: whitelisted keys, dict access - llm_analysis_service: rate_limit parse mock - migration_plugin: retry side_effect exhaustion - preview: DB query instead of dict key - scheduler: UTC→None for SQLite naive datetimes, patch targets, async wrappers NEW TEST FILES (10+): - scripts/: check_migration_chain, seed_superset_load_test, test_dataset_dashboard_relations, create_admin, seed_permissions, init_auth_db, delete_running_tasks - llm_analysis: plugin_coverage +5, service_coverage +5, migration +2 - clean_release_ext +9, superset_compilation_adapter_edge +5 - service_inline_correction +7 (via __tests__) MODULES AT 100%: clean_release models, superset_compilation_adapter, service_inline_correction, llm_analysis/plugin, dependencies DEAD CODE DOCUMENTED: search.py (L206-215 indentation bug), llm_analysis/service (L459 HTTPS, L594 duplicate tab, L639-697 CDP-only)
298 lines
12 KiB
Python
298 lines
12 KiB
Python
# #region TestSeedSupersetLoadTest [C:2] [TYPE Module] [SEMANTICS test,superset,seed,load-test]
|
|
# @BRIEF Tests for pure logic functions in seed_superset_load_test.py (no real Superset API).
|
|
# @RELATION BINDS_TO -> [SeedSupersetLoadTestScript]
|
|
# @TEST_EDGE: parse_args_defaults -> Returns default values
|
|
# @TEST_EDGE: parse_args_custom -> Overrides with CLI args
|
|
# @TEST_EDGE: extract_result_payload_wrapped -> Unwraps 'result' key
|
|
# @TEST_EDGE: extract_result_payload_plain -> Returns payload as-is
|
|
# @TEST_EDGE: extract_result_payload_not_dict -> Returns payload
|
|
# @TEST_EDGE: extract_created_id_direct -> Reads id key
|
|
# @TEST_EDGE: extract_created_id_nested -> Reads result.id
|
|
# @TEST_EDGE: extract_created_id_missing -> Returns None
|
|
# @TEST_EDGE: generate_unique_name_unique -> No collisions
|
|
# @TEST_EDGE: generate_unique_name_collision -> Skips used names
|
|
# @TEST_EDGE: resolve_target_envs_ok -> Returns resolved envs
|
|
# @TEST_EDGE: resolve_target_envs_missing -> Raises ValueError
|
|
# @TEST_EDGE: seed_superset_load_data_dry_run -> No side effects with --dry-run
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
|
|
# =============================================================================
|
|
# _parse_args
|
|
# =============================================================================
|
|
|
|
class TestParseArgs:
|
|
"""Tests for seed_superset_load_test._parse_args()."""
|
|
|
|
def test_defaults(self):
|
|
"""Happy: no CLI args returns defaults."""
|
|
from src.scripts.seed_superset_load_test import _parse_args
|
|
with patch("sys.argv", ["prog"]):
|
|
args = _parse_args()
|
|
assert args.envs == ["ss1", "ss2"]
|
|
assert args.charts == 10000
|
|
assert args.dashboards == 500
|
|
assert args.template_pool_size == 200
|
|
assert args.seed is None
|
|
assert args.max_errors == 100
|
|
assert args.dry_run is False
|
|
|
|
def test_custom_values(self):
|
|
"""Edge: all args overridden."""
|
|
from src.scripts.seed_superset_load_test import _parse_args
|
|
argv = [
|
|
"prog",
|
|
"--envs", "prod", "staging",
|
|
"--charts", "50",
|
|
"--dashboards", "10",
|
|
"--template-pool-size", "5",
|
|
"--seed", "42",
|
|
"--max-errors", "5",
|
|
"--dry-run",
|
|
]
|
|
with patch("sys.argv", argv):
|
|
args = _parse_args()
|
|
assert args.envs == ["prod", "staging"]
|
|
assert args.charts == 50
|
|
assert args.dashboards == 10
|
|
assert args.template_pool_size == 5
|
|
assert args.seed == 42
|
|
assert args.max_errors == 5
|
|
assert args.dry_run is True
|
|
|
|
|
|
# =============================================================================
|
|
# _extract_result_payload
|
|
# =============================================================================
|
|
|
|
class TestExtractResultPayload:
|
|
"""Tests for seed_superset_load_test._extract_result_payload()."""
|
|
|
|
def test_wrapped_in_result(self):
|
|
"""Happy: payload has result dict, returns it."""
|
|
from src.scripts.seed_superset_load_test import _extract_result_payload
|
|
payload = {"result": {"id": 1, "name": "test"}}
|
|
result = _extract_result_payload(payload)
|
|
assert result == {"id": 1, "name": "test"}
|
|
|
|
def test_plain_payload(self):
|
|
"""Edge: no result key returns payload as-is."""
|
|
from src.scripts.seed_superset_load_test import _extract_result_payload
|
|
payload = {"id": 1, "name": "test"}
|
|
result = _extract_result_payload(payload)
|
|
assert result == payload
|
|
|
|
def test_result_not_dict(self):
|
|
"""Edge: result is a list, returns original payload."""
|
|
from src.scripts.seed_superset_load_test import _extract_result_payload
|
|
payload = {"result": [1, 2, 3]}
|
|
result = _extract_result_payload(payload)
|
|
assert result == payload
|
|
|
|
|
|
# =============================================================================
|
|
# _extract_created_id
|
|
# =============================================================================
|
|
|
|
class TestExtractCreatedId:
|
|
"""Tests for seed_superset_load_test._extract_created_id()."""
|
|
|
|
def test_direct_id(self):
|
|
"""Happy: payload has direct id key."""
|
|
from src.scripts.seed_superset_load_test import _extract_created_id
|
|
assert _extract_created_id({"id": 42}) == 42
|
|
|
|
def test_nested_in_result(self):
|
|
"""Happy: id nested under result key."""
|
|
from src.scripts.seed_superset_load_test import _extract_created_id
|
|
assert _extract_created_id({"result": {"id": 99}}) == 99
|
|
|
|
def test_missing_id(self):
|
|
"""Edge: no id anywhere returns None."""
|
|
from src.scripts.seed_superset_load_test import _extract_created_id
|
|
assert _extract_created_id({"foo": "bar"}) is None
|
|
|
|
def test_id_not_int(self):
|
|
"""Edge: id is non-int returns None."""
|
|
from src.scripts.seed_superset_load_test import _extract_created_id
|
|
assert _extract_created_id({"id": "abc"}) is None
|
|
|
|
def test_result_without_id(self):
|
|
"""Edge: result exists but has no id."""
|
|
from src.scripts.seed_superset_load_test import _extract_created_id
|
|
assert _extract_created_id({"result": {"foo": "bar"}}) is None
|
|
|
|
|
|
# =============================================================================
|
|
# _generate_unique_name
|
|
# =============================================================================
|
|
|
|
class TestGenerateUniqueName:
|
|
"""Tests for seed_superset_load_test._generate_unique_name()."""
|
|
|
|
def test_generates_unique_names(self):
|
|
"""Happy: generates multiple unique names without collision."""
|
|
from src.scripts.seed_superset_load_test import _generate_unique_name
|
|
import random
|
|
rng = random.Random(42)
|
|
used: set[str] = set()
|
|
names = [_generate_unique_name("lt_chart", used, rng) for _ in range(50)]
|
|
assert len(names) == 50
|
|
assert len(set(names)) == 50 # all unique
|
|
assert all(n.startswith("lt_chart_") for n in names)
|
|
|
|
def test_collision_avoidance(self):
|
|
"""Edge: generates name, verifies it's added to used set."""
|
|
from src.scripts.seed_superset_load_test import _generate_unique_name
|
|
import random
|
|
rng = random.Random(42)
|
|
used: set[str] = set()
|
|
|
|
# Verify the function adds the name to used and returns a valid name
|
|
name = _generate_unique_name("lt_test", used, rng)
|
|
assert name.startswith("lt_test_")
|
|
assert len(name) > len("lt_test_")
|
|
# The name is added to used by the function
|
|
assert name in used
|
|
# Generate another — it should be different
|
|
name2 = _generate_unique_name("lt_test", used, rng)
|
|
assert name2 != name
|
|
assert name2 in used
|
|
assert len(used) == 2
|
|
|
|
|
|
# =============================================================================
|
|
# _resolve_target_envs (mocked config)
|
|
# =============================================================================
|
|
|
|
class TestResolveTargetEnvs:
|
|
"""Tests for seed_superset_load_test._resolve_target_envs()."""
|
|
|
|
def _make_env(self, env_id: str, name: str = ""):
|
|
"""Helper to build a minimal Environment with required fields."""
|
|
from src.core.config_models import Environment
|
|
return Environment(
|
|
id=env_id,
|
|
name=name or env_id,
|
|
url=f"http://{env_id}.test",
|
|
username="test_user",
|
|
password="test_pass",
|
|
)
|
|
|
|
@patch("src.scripts.seed_superset_load_test.ConfigManager")
|
|
def test_resolves_configured_envs(self, MockConfigManager):
|
|
"""Happy: returns resolved env objects."""
|
|
from src.scripts.seed_superset_load_test import _resolve_target_envs
|
|
|
|
env1 = self._make_env("ss1", "Env1")
|
|
env2 = self._make_env("ss2", "Env2")
|
|
|
|
mock_mgr = MockConfigManager.return_value
|
|
mock_mgr.get_environments.return_value = [env1, env2]
|
|
|
|
result = _resolve_target_envs(["ss1", "ss2"])
|
|
assert "ss1" in result
|
|
assert "ss2" in result
|
|
assert result["ss1"].name == "Env1"
|
|
|
|
@patch("src.scripts.seed_superset_load_test.ConfigManager")
|
|
def test_missing_env_raises(self, MockConfigManager):
|
|
"""Edge: requested env not in config raises ValueError."""
|
|
from src.scripts.seed_superset_load_test import _resolve_target_envs
|
|
|
|
env1 = self._make_env("ss1")
|
|
|
|
mock_mgr = MockConfigManager.return_value
|
|
mock_mgr.get_environments.return_value = [env1]
|
|
|
|
with pytest.raises(ValueError, match="Environment 'ss2' not found"):
|
|
_resolve_target_envs(["ss1", "ss2"])
|
|
|
|
@patch("src.scripts.seed_superset_load_test.ConfigManager")
|
|
def test_no_environments_empty_result(self, MockConfigManager):
|
|
"""Edge: no configured envs returns empty dict, no error for empty request."""
|
|
from src.scripts.seed_superset_load_test import _resolve_target_envs
|
|
|
|
mock_mgr = MockConfigManager.return_value
|
|
mock_mgr.get_environments.return_value = []
|
|
|
|
# Empty request: no error
|
|
result = _resolve_target_envs([])
|
|
assert result == {}
|
|
|
|
@patch("src.scripts.seed_superset_load_test.ConfigManager")
|
|
def test_partial_resolution(self, MockConfigManager):
|
|
"""Edge: only some envs resolved, raises on missing."""
|
|
from src.scripts.seed_superset_load_test import _resolve_target_envs
|
|
|
|
env1 = self._make_env("ss1")
|
|
|
|
mock_mgr = MockConfigManager.return_value
|
|
mock_mgr.get_environments.return_value = [env1]
|
|
|
|
with pytest.raises(ValueError, match="Environment 'ss2' not found"):
|
|
_resolve_target_envs(["ss1", "ss2"])
|
|
|
|
|
|
# =============================================================================
|
|
# seed_superset_load_data (dry_run mode only — no real API calls)
|
|
# =============================================================================
|
|
|
|
class TestSeedSupersetLoadData:
|
|
"""Tests for seed_superset_load_data — dry run and early returns."""
|
|
|
|
def _make_env(self, env_id: str, name: str = ""):
|
|
"""Helper to build a minimal Environment with required fields."""
|
|
from src.core.config_models import Environment
|
|
return Environment(
|
|
id=env_id,
|
|
name=name or env_id,
|
|
url=f"http://{env_id}.test",
|
|
username="test_user",
|
|
password="test_pass",
|
|
)
|
|
|
|
@patch("src.scripts.seed_superset_load_test._resolve_target_envs")
|
|
@patch("src.scripts.seed_superset_load_test.SupersetClient")
|
|
def test_dry_run_returns_summary(self, MockClient, mock_resolve):
|
|
"""Happy: dry_run=True returns summary without API writes."""
|
|
from src.scripts.seed_superset_load_test import seed_superset_load_data
|
|
|
|
env1 = self._make_env("ss1", "Env1")
|
|
mock_resolve.return_value = {"ss1": env1}
|
|
|
|
# Mock client
|
|
mock_client = MagicMock()
|
|
mock_client.network.fetch_paginated_data.return_value = [
|
|
{"id": 1, "slice_name": "chart1", "viz_type": "bar"}
|
|
]
|
|
mock_client.get_chart.return_value = {
|
|
"result": {"datasource_id": 10, "datasource_type": "table", "params": "{}"}
|
|
}
|
|
MockClient.return_value = mock_client
|
|
|
|
args = MagicMock()
|
|
args.seed = 42
|
|
args.envs = ["ss1"]
|
|
args.charts = 100
|
|
args.dashboards = 10
|
|
args.template_pool_size = 5
|
|
args.max_errors = 100
|
|
args.dry_run = True
|
|
|
|
result = seed_superset_load_data(args)
|
|
|
|
assert result["dry_run"] is True
|
|
assert result["charts_target"] == 100
|
|
assert result["dashboards_target"] == 10
|
|
assert result["templates_by_env"] == {"ss1": 1}
|
|
# In dry_run mode, no POST requests are made
|
|
mock_client.network.request.assert_not_called()
|
|
# #endregion TestSeedSupersetLoadTest
|