| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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. |
| """Tests for examples/utils.py - YAML config loading and content assembly.""" |
| |
| from pathlib import Path |
| from tempfile import TemporaryDirectory |
| from unittest.mock import MagicMock, patch |
| |
| import yaml |
| |
| |
| def _create_example_tree(base_dir: Path) -> Path: |
| """Create a minimal example directory tree under base_dir/superset/examples/. |
| |
| Returns the 'superset' directory (what files("superset") would return). |
| """ |
| superset_dir = base_dir / "superset" |
| examples_dir = superset_dir / "examples" |
| |
| # _shared configs |
| shared_dir = examples_dir / "_shared" |
| shared_dir.mkdir(parents=True) |
| (shared_dir / "database.yaml").write_text( |
| "database_name: examples\n" |
| "sqlalchemy_uri: __SQLALCHEMY_EXAMPLES_URI__\n" |
| "uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee\n" |
| "version: '1.0.0'\n" |
| ) |
| (shared_dir / "metadata.yaml").write_text( |
| "version: '1.0.0'\ntimestamp: '2020-12-11T22:52:56.534241+00:00'\n" |
| ) |
| |
| # An example with dataset, dashboard, and chart |
| example_dir = examples_dir / "test_example" |
| example_dir.mkdir() |
| (example_dir / "dataset.yaml").write_text( |
| yaml.dump( |
| { |
| "table_name": "test_table", |
| "schema": "main", |
| "uuid": "14f48794-ebfa-4f60-a26a-582c49132f1b", |
| "database_uuid": "a2dc77af-e654-49bb-b321-40f6b559a1ee", |
| "version": "1.0.0", |
| } |
| ) |
| ) |
| (example_dir / "dashboard.yaml").write_text( |
| yaml.dump( |
| { |
| "dashboard_title": "Test Dashboard", |
| "uuid": "dddddddd-dddd-dddd-dddd-dddddddddddd", |
| "version": "1.0.0", |
| } |
| ) |
| ) |
| charts_dir = example_dir / "charts" |
| charts_dir.mkdir() |
| (charts_dir / "test_chart.yaml").write_text( |
| yaml.dump( |
| { |
| "slice_name": "Test Chart", |
| "uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc", |
| "dataset_uuid": "14f48794-ebfa-4f60-a26a-582c49132f1b", |
| "version": "1.0.0", |
| } |
| ) |
| ) |
| |
| return superset_dir |
| |
| |
| def test_load_contents_builds_correct_import_structure(): |
| """load_contents() must produce the key structure ImportExamplesCommand expects. |
| |
| This tests the orchestration entry point: YAML files are discovered from |
| the examples directory, the shared database config has its URI placeholder |
| replaced, and the result has the correct key prefixes (databases/, datasets/, |
| metadata.yaml). |
| """ |
| from superset.examples.utils import load_contents |
| |
| with TemporaryDirectory() as tmpdir: |
| superset_dir = _create_example_tree(Path(tmpdir)) |
| |
| test_examples_uri = "sqlite:///path/to/examples.db" |
| mock_app = MagicMock() |
| mock_app.config = {"SQLALCHEMY_EXAMPLES_URI": test_examples_uri} |
| |
| with patch("superset.examples.utils.files", return_value=superset_dir): |
| with patch("flask.current_app", mock_app): |
| contents = load_contents() |
| |
| # Verify database config is present with placeholder replaced |
| assert "databases/examples.yaml" in contents |
| db_content = contents["databases/examples.yaml"] |
| assert "__SQLALCHEMY_EXAMPLES_URI__" not in db_content |
| assert test_examples_uri in db_content |
| |
| # Verify metadata is present |
| assert "metadata.yaml" in contents |
| |
| # Verify dataset is discovered with correct key prefix |
| assert "datasets/examples/test_example.yaml" in contents |
| |
| # Verify dashboard is discovered with correct key prefix |
| assert "dashboards/test_example.yaml" in contents |
| |
| # Verify chart is discovered with correct key prefix |
| assert "charts/test_example/test_chart.yaml" in contents |
| |
| # Verify schema normalization happened (main -> null) |
| dataset_content = contents["datasets/examples/test_example.yaml"] |
| assert "schema: main" not in dataset_content |
| assert "schema: null" in dataset_content |
| |
| |
| def test_load_contents_replaces_sqlalchemy_examples_uri_placeholder(): |
| """The __SQLALCHEMY_EXAMPLES_URI__ placeholder must be replaced with the real URI. |
| |
| If this placeholder is not replaced, the database import will fail with an |
| invalid connection string, preventing all examples from loading. |
| """ |
| from superset.examples.utils import _load_shared_configs |
| |
| with TemporaryDirectory() as tmpdir: |
| superset_dir = _create_example_tree(Path(tmpdir)) |
| examples_root = Path("examples") |
| |
| test_uri = "postgresql://user:pass@host/db" |
| mock_app = MagicMock() |
| mock_app.config = {"SQLALCHEMY_EXAMPLES_URI": test_uri} |
| |
| with patch("superset.examples.utils.files", return_value=superset_dir): |
| with patch("flask.current_app", mock_app): |
| contents = _load_shared_configs(examples_root) |
| |
| assert "databases/examples.yaml" in contents |
| assert test_uri in contents["databases/examples.yaml"] |
| assert "__SQLALCHEMY_EXAMPLES_URI__" not in contents["databases/examples.yaml"] |
| |
| |
| @patch("superset.examples.utils.ImportExamplesCommand") |
| @patch("superset.examples.utils.load_contents") |
| def test_load_examples_from_configs_wires_command_correctly( |
| mock_load_contents, |
| mock_command_cls, |
| ): |
| """load_examples_from_configs() must construct ImportExamplesCommand |
| with overwrite=True and thread force_data through. |
| |
| A wiring regression here would silently skip overwriting existing |
| examples or ignore the force_data flag. |
| """ |
| from superset.examples.utils import load_examples_from_configs |
| |
| mock_load_contents.return_value = {"databases/examples.yaml": "content"} |
| mock_command = MagicMock() |
| mock_command_cls.return_value = mock_command |
| |
| load_examples_from_configs(force_data=True) |
| |
| mock_load_contents.assert_called_once_with(False) |
| mock_command_cls.assert_called_once_with( |
| {"databases/examples.yaml": "content"}, |
| overwrite=True, |
| force_data=True, |
| ) |
| mock_command.run.assert_called_once() |
| |
| |
| @patch("superset.examples.utils.ImportExamplesCommand") |
| @patch("superset.examples.utils.load_contents") |
| def test_load_examples_from_configs_defaults( |
| mock_load_contents, |
| mock_command_cls, |
| ): |
| """Default call should pass force_data=False and load_test_data=False.""" |
| from superset.examples.utils import load_examples_from_configs |
| |
| mock_load_contents.return_value = {} |
| mock_command = MagicMock() |
| mock_command_cls.return_value = mock_command |
| |
| load_examples_from_configs() |
| |
| mock_load_contents.assert_called_once_with(False) |
| mock_command_cls.assert_called_once_with( |
| {}, |
| overwrite=True, |
| force_data=False, |
| ) |
| mock_command.run.assert_called_once() |