|
12 | 12 | from click.testing import CliRunner |
13 | 13 | from sqlglot import __version__ as SQLGLOT_VERSION |
14 | 14 | from sqlmesh import RuntimeEnv |
| 15 | +from sqlmesh.cli import main as cli_main |
15 | 16 | from sqlmesh._version import __version__ as SQLMESH_VERSION |
16 | 17 | from sqlmesh.cli.project_init import ProjectTemplate, init_example_project |
17 | 18 | from sqlmesh.cli.main import cli |
@@ -2582,6 +2583,187 @@ def _setup_local_only_project(tmp_path, mocker): |
2582 | 2583 | return _patch_state_access(mocker) |
2583 | 2584 |
|
2584 | 2585 |
|
| 2586 | +@pytest.fixture |
| 2587 | +def cli_env(monkeypatch): |
| 2588 | + for name in ( |
| 2589 | + "SQLMESH_PATHS", |
| 2590 | + "SQLMESH_CONFIG", |
| 2591 | + "SQLMESH_TEST_LOCAL", |
| 2592 | + "SQLMESH_LINT_LOCAL", |
| 2593 | + "SQLMESH_DOTENV_PATH", |
| 2594 | + "SQLMESH__MODEL_DEFAULTS__DIALECT", |
| 2595 | + ): |
| 2596 | + # Register an undo even when absent, since dotenv can add the variable directly. |
| 2597 | + monkeypatch.setenv(name, "") |
| 2598 | + monkeypatch.delenv(name) |
| 2599 | + |
| 2600 | + |
| 2601 | +@pytest.mark.parametrize("name", ["project", "analytics project 分析"]) |
| 2602 | +@pytest.mark.parametrize("relative", [False, True]) |
| 2603 | +def test_paths_env_selects_project_from_another_directory( |
| 2604 | + runner, tmp_path, monkeypatch, cli_env, name, relative |
| 2605 | +): |
| 2606 | + project = tmp_path / name |
| 2607 | + _create_local_only_project(project, "env_project") |
| 2608 | + elsewhere = tmp_path / "elsewhere" |
| 2609 | + elsewhere.mkdir() |
| 2610 | + monkeypatch.chdir(elsewhere) |
| 2611 | + monkeypatch.setenv("SQLMESH_PATHS", os.path.relpath(project) if relative else str(project)) |
| 2612 | + result = runner.invoke(cli, ["--log-file-dir", str(tmp_path), "info"]) |
| 2613 | + assert result.exit_code == 0, result.output |
| 2614 | + assert "Models: 1" in result.output |
| 2615 | + |
| 2616 | + |
| 2617 | +@pytest.mark.parametrize("command", ["test", "lint"]) |
| 2618 | +@pytest.mark.parametrize("value", ["true", "1", "false", "0", "", "invalid"]) |
| 2619 | +@pytest.mark.parametrize("explicit", [False, True]) |
| 2620 | +def test_local_env_state_gating( |
| 2621 | + runner, tmp_path, mocker, monkeypatch, cli_env, command, value, explicit |
| 2622 | +): |
| 2623 | + state = _setup_local_only_project(tmp_path, mocker) |
| 2624 | + init = mocker.spy(Context, "__init__") |
| 2625 | + monkeypatch.setenv(f"SQLMESH_{command.upper()}_LOCAL", value) |
| 2626 | + args = ["--paths", str(tmp_path), command] |
| 2627 | + if explicit: |
| 2628 | + args.append("--local") |
| 2629 | + result = runner.invoke(cli, args) |
| 2630 | + if value == "invalid" and not explicit: |
| 2631 | + assert result.exit_code == 2, result.output |
| 2632 | + assert "valid boolean" in result.output |
| 2633 | + init.assert_not_called() |
| 2634 | + state.assert_not_called() |
| 2635 | + elif explicit or value in ("true", "1"): |
| 2636 | + assert result.exit_code == 0, result.output |
| 2637 | + assert init.call_args.kwargs["load_state"] is False |
| 2638 | + state.assert_not_called() |
| 2639 | + else: |
| 2640 | + assert init.call_args.kwargs["load_state"] is True |
| 2641 | + state.assert_called() |
| 2642 | + |
| 2643 | + |
| 2644 | +@pytest.mark.parametrize("command", ["test", "lint"]) |
| 2645 | +@pytest.mark.parametrize("custom_dotenv", [False, True]) |
| 2646 | +@pytest.mark.parametrize("value", ["true", "false"]) |
| 2647 | +def test_local_env_from_dotenv( |
| 2648 | + runner, tmp_path, mocker, monkeypatch, cli_env, command, custom_dotenv, value |
| 2649 | +): |
| 2650 | + state = _setup_local_only_project(tmp_path, mocker) |
| 2651 | + init = mocker.spy(Context, "__init__") |
| 2652 | + variable = f"SQLMESH_{command.upper()}_LOCAL" |
| 2653 | + # The existing config loader lets dotenv override the process environment. |
| 2654 | + monkeypatch.setenv(variable, "false" if value == "true" else "true") |
| 2655 | + env_file = tmp_path / ("custom.env" if custom_dotenv else ".env") |
| 2656 | + env_file.write_text(f"{variable}={value}\n", encoding="utf-8") |
| 2657 | + args = ["--paths", str(tmp_path)] |
| 2658 | + if custom_dotenv: |
| 2659 | + args.extend(["--dotenv", str(env_file)]) |
| 2660 | + result = runner.invoke(cli, [*args, command]) |
| 2661 | + assert init.call_args.kwargs["load_state"] is (value == "false") |
| 2662 | + if value == "true": |
| 2663 | + assert result.exit_code == 0, result.output |
| 2664 | + state.assert_not_called() |
| 2665 | + else: |
| 2666 | + state.assert_called() |
| 2667 | + |
| 2668 | + |
| 2669 | +@pytest.mark.parametrize("command", ["test", "lint", "format"]) |
| 2670 | +def test_paths_env_multiple_projects_local(runner, tmp_path, mocker, monkeypatch, cli_env, command): |
| 2671 | + projects = (tmp_path / "first project", tmp_path / "second 分析") |
| 2672 | + for index, project in enumerate(projects): |
| 2673 | + _create_local_only_project(project, f"project_{index}") |
| 2674 | + state = _patch_state_access(mocker) |
| 2675 | + monkeypatch.setenv("SQLMESH_PATHS", os.pathsep.join(str(project) for project in projects)) |
| 2676 | + if command != "format": |
| 2677 | + monkeypatch.setenv(f"SQLMESH_{command.upper()}_LOCAL", "true") |
| 2678 | + result = runner.invoke(cli, [command]) |
| 2679 | + assert result.exit_code == 0, result.output |
| 2680 | + state.assert_not_called() |
| 2681 | + |
| 2682 | + |
| 2683 | +def test_paths_env_init_new_destination(runner, tmp_path, monkeypatch, cli_env): |
| 2684 | + project = tmp_path / "new project" |
| 2685 | + project.mkdir() |
| 2686 | + monkeypatch.setenv("SQLMESH_PATHS", str(project)) |
| 2687 | + result = runner.invoke(cli, ["init", "duckdb", "--template", "empty"]) |
| 2688 | + assert result.exit_code == 0, result.output |
| 2689 | + assert (project / "config.yaml").exists() |
| 2690 | + |
| 2691 | + |
| 2692 | +@pytest.mark.parametrize("invalid_config", [False, True]) |
| 2693 | +def test_paths_env_project_errors(runner, tmp_path, monkeypatch, cli_env, invalid_config): |
| 2694 | + project = tmp_path / "project" |
| 2695 | + if invalid_config: |
| 2696 | + project.mkdir() |
| 2697 | + (project / "config.yaml").write_text("not a config\n", encoding="utf-8") |
| 2698 | + monkeypatch.setenv("SQLMESH_PATHS", str(project)) |
| 2699 | + result = runner.invoke(cli, ["info"]) |
| 2700 | + assert result.exit_code == 1, result.output |
| 2701 | + assert ( |
| 2702 | + "Invalid YAML configuration" if invalid_config else "project config could not be found" |
| 2703 | + ) in result.output |
| 2704 | + |
| 2705 | + |
| 2706 | +def test_paths_env_in_dotenv_does_not_change_root_paths(runner, tmp_path, monkeypatch, cli_env): |
| 2707 | + project = tmp_path / "project" |
| 2708 | + alternate = tmp_path / "alternate" |
| 2709 | + _create_local_only_project(project, "selected_project") |
| 2710 | + _create_local_only_project(alternate, "alternate_project") |
| 2711 | + (alternate / "models" / "second.sql").write_text( |
| 2712 | + "MODEL(name alternate_project.second); SELECT 2 AS col", encoding="utf-8" |
| 2713 | + ) |
| 2714 | + (project / ".env").write_text(f"SQLMESH_PATHS='{alternate}'\n", encoding="utf-8") |
| 2715 | + monkeypatch.setenv("SQLMESH_PATHS", str(project)) |
| 2716 | + result = runner.invoke(cli, ["info"]) |
| 2717 | + assert result.exit_code == 0, result.output |
| 2718 | + assert "Models: 1" in result.output |
| 2719 | + |
| 2720 | + |
| 2721 | +def test_paths_env_only_in_custom_dotenv_does_not_select_project( |
| 2722 | + runner, tmp_path, monkeypatch, mocker, cli_env |
| 2723 | +): |
| 2724 | + project = tmp_path / "project" |
| 2725 | + _create_local_only_project(project, "env_project") |
| 2726 | + env_file = tmp_path / "custom.env" |
| 2727 | + env_file.write_text(f"SQLMESH_PATHS='{project}'\n", encoding="utf-8") |
| 2728 | + load_configs = mocker.spy(cli_main, "load_configs") |
| 2729 | + runner.invoke(cli, ["--dotenv", str(env_file), "info"]) |
| 2730 | + paths = next(param for param in cli.params if param.name == "paths") |
| 2731 | + assert load_configs.call_args.args[2] == tuple(paths.default) |
| 2732 | + |
| 2733 | + |
| 2734 | +@pytest.mark.parametrize("empty_segment", [False, True]) |
| 2735 | +def test_paths_env_preserves_loader_expansion( |
| 2736 | + runner, tmp_path, monkeypatch, mocker, cli_env, empty_segment |
| 2737 | +): |
| 2738 | + projects = [tmp_path / "projects" / "first", tmp_path / "projects" / "second"] |
| 2739 | + for index, project in enumerate(projects): |
| 2740 | + _create_local_only_project(project, f"env_project_{index}") |
| 2741 | + if empty_segment: |
| 2742 | + invocation = tmp_path / "invocation" |
| 2743 | + _create_local_only_project(invocation, "invocation") |
| 2744 | + monkeypatch.chdir(invocation) |
| 2745 | + env_value = os.pathsep.join([str(projects[0]), "", str(projects[1])]) |
| 2746 | + expected = {projects[0], invocation, projects[1]} |
| 2747 | + else: |
| 2748 | + env_value = str(tmp_path / "projects" / "*") |
| 2749 | + expected = set(projects) |
| 2750 | + monkeypatch.setenv("SQLMESH_PATHS", env_value) |
| 2751 | + init = mocker.spy(Context, "__init__") |
| 2752 | + result = runner.invoke(cli, ["format"]) |
| 2753 | + assert result.exit_code == 0, result.output |
| 2754 | + assert set(init.call_args.kwargs["config"]) == expected |
| 2755 | + |
| 2756 | + |
| 2757 | +def test_cli_env_separate_from_project_config(runner, tmp_path, monkeypatch, mocker, cli_env): |
| 2758 | + _create_local_only_project(tmp_path, "env_project") |
| 2759 | + monkeypatch.setenv("SQLMESH_PATHS", str(tmp_path)) |
| 2760 | + monkeypatch.setenv("SQLMESH__MODEL_DEFAULTS__DIALECT", "spark") |
| 2761 | + init = mocker.spy(Context, "__init__") |
| 2762 | + result = runner.invoke(cli, ["info"]) |
| 2763 | + assert result.exit_code == 0, result.output |
| 2764 | + assert next(iter(init.call_args.kwargs["config"].values())).model_defaults.dialect == "spark" |
| 2765 | + |
| 2766 | + |
2585 | 2767 | def test_format_runs_without_state(runner: CliRunner, tmp_path: Path, mocker): |
2586 | 2768 | mock = _setup_local_only_project(tmp_path, mocker) |
2587 | 2769 | result = runner.invoke(cli, ["--paths", str(tmp_path), "format"]) |
|
0 commit comments