Skip to content

Commit 3b090c5

Browse files
committed
test(cli): cover --local against real unit tests and a multi-repo project
Adds the three cases asked for in review: - a real unit test from the example project's YAML runs under --local, exits 0 and never reaches state sync - a twin of test_format_does_not_open_state_connection, so a configured remote Postgres state connection is not opened when its env vars are unset - examples/multi with only repo_2 given, which pins the behavioural difference against lint: the same run reaches state without --local, and with --local the test for bronze.a (defined in repo_1, so not loaded) warns and is skipped while repo_2's own test still runs Also corrects the documented behaviour. The description said --local "may cause errors"; in fact create_test logs a warning and returns None when a model is missing, so the suite passes. The docs now say the test is skipped with a warning, show the output, and point out that a green exit code does not mean every expected test ran. The --local help text carries the same correction, and the entry on the CLI reference page is trimmed back to mirror the real --help output rather than adding prose the command never prints. Signed-off-by: Adegbite Ayoade <tripleaceme@gmail.com>
1 parent e800d6d commit 3b090c5

4 files changed

Lines changed: 125 additions & 7 deletions

File tree

‎docs/concepts/tests.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,16 @@ You can pass `--local` to run tests without loading state from the configured st
469469
$ sqlmesh test --local
470470
```
471471

472-
This keeps offline runs and commit hooks from opening a connection to the state backend. As with [`sqlmesh lint --local`](../guides/linter.md), in multi-repository setups, or when running tests for only a subset of projects, `--local` may cause errors because SQLMesh will not resolve references or schemas from models that exist only in remote state.
472+
This keeps offline runs and commit hooks from opening a connection to the state backend.
473+
474+
In multi-repository setups, or when running tests for only a subset of projects, models that exist only in remote state are not loaded under `--local`. Unlike [`sqlmesh lint --local`](../guides/linter.md), which reports additional errors in that situation, a test whose model is missing is **skipped with a warning and the run still succeeds**:
475+
476+
```
477+
[WARNING] Model '"memory"."bronze"."a"' was not found at tests/test_a.yaml
478+
.**Successfully Ran `1` Tests Against `duckdb`**
479+
```
480+
481+
So a passing exit code alone does not mean every test you expected actually ran. Watch the output for these warnings, and keep in mind that a hook using `--local` will not fail on them.
473482

474483
### Testing using notebooks
475484

‎docs/reference/cli.md‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,8 @@ Options:
631631
--select-model TEXT Select specific models to run unit tests for. Can be
632632
specified multiple times.
633633
--local Run tests using only locally loaded project files
634-
without loading state. In multi-repository setups, or
635-
when running tests for only a subset of projects, this
636-
may cause errors because SQLMesh will not resolve
637-
references or schemas from models that exist only in
638-
remote state.
634+
without loading state. Tests whose model is not loaded
635+
are skipped with a warning rather than failing.
639636
--help Show this message and exit.
640637
```
641638

‎sqlmesh/cli/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ def create_test(
821821
"--local",
822822
is_flag=True,
823823
expose_value=False,
824-
help="Run tests using only locally loaded project files without loading state.",
824+
help="Run tests using only locally loaded project files without loading state. Tests whose model is not loaded are skipped with a warning rather than failing.",
825825
)
826826
@click.argument("tests", nargs=-1)
827827
@click.pass_obj

‎tests/cli/test_cli.py‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2664,3 +2664,115 @@ def test_format_does_not_open_state_connection(
26642664
result = runner.invoke(cli, ["--paths", str(tmp_path), "format"])
26652665
assert result.exit_code == 0, f"Format failed: {result.output}\nException: {result.exception}"
26662666
mock.assert_not_called()
2667+
2668+
2669+
def test_test_local_runs_project_unit_tests(runner: CliRunner, tmp_path: Path, mocker) -> None:
2670+
"""A real unit test from the project's YAML runs under `--local` without touching state."""
2671+
create_example_project(tmp_path)
2672+
mock = _patch_state_access(mocker)
2673+
2674+
result = runner.invoke(cli, ["--paths", str(tmp_path), "test", "--local"])
2675+
2676+
assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}"
2677+
assert "Successfully Ran 1 tests" in " ".join(result.output.split())
2678+
mock.assert_not_called()
2679+
2680+
2681+
def test_test_local_does_not_open_state_connection(
2682+
runner: CliRunner, tmp_path: Path, mocker, monkeypatch
2683+
) -> None:
2684+
"""`test --local` must not open a configured remote Postgres state connection."""
2685+
pytest.importorskip("psycopg2")
2686+
2687+
for var in ("PG_HOST", "PG_USER", "PG_PASSWORD", "PG_DATABASE"):
2688+
monkeypatch.delenv(var, raising=False)
2689+
2690+
create_example_project(tmp_path)
2691+
(tmp_path / "config.yaml").write_text(
2692+
"""project: cli_test
2693+
2694+
gateways:
2695+
prod:
2696+
state_connection:
2697+
type: postgres
2698+
host: "{{ env_var('PG_HOST', 'postgres.internal.example.com') }}"
2699+
port: 5432
2700+
user: "{{ env_var('PG_USER') }}"
2701+
password: "{{ env_var('PG_PASSWORD') }}"
2702+
database: "{{ env_var('PG_DATABASE', 'sqlmesh_state') }}"
2703+
connection:
2704+
type: duckdb
2705+
database: "warehouse.db"
2706+
2707+
default_gateway: prod
2708+
2709+
model_defaults:
2710+
dialect: duckdb
2711+
""",
2712+
encoding="utf-8",
2713+
)
2714+
2715+
mock = _patch_state_access(mocker)
2716+
2717+
result = runner.invoke(cli, ["--paths", str(tmp_path), "test", "--local"])
2718+
assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}"
2719+
mock.assert_not_called()
2720+
2721+
2722+
def test_test_local_multi_repo_partial(runner: CliRunner, copy_to_temp_path, mocker) -> None:
2723+
"""Run tests for one repo of a multi-repo project whose upstream models live only in state.
2724+
2725+
Pins the behavioral difference against `lint --local`: a model that isn't loaded produces a
2726+
warning and its test is skipped, rather than turning into an error.
2727+
"""
2728+
repo_2 = copy_to_temp_path("examples/multi")[0] / "repo_2"
2729+
2730+
# silver.c lives in repo_2 and its upstream bronze.a is supplied as a test input.
2731+
(repo_2 / "tests" / "test_c.yaml").write_text(
2732+
"""test_silver_c:
2733+
model: silver.c
2734+
inputs:
2735+
bronze.a:
2736+
rows:
2737+
- col_a: 1
2738+
- col_a: 1
2739+
- col_a: 2
2740+
outputs:
2741+
query:
2742+
rows:
2743+
- col_a: 1
2744+
- col_a: 2
2745+
""",
2746+
encoding="utf-8",
2747+
)
2748+
# bronze.a itself is defined in repo_1, so it is not loaded when only repo_2 is given.
2749+
(repo_2 / "tests" / "test_a.yaml").write_text(
2750+
"""test_bronze_a:
2751+
model: bronze.a
2752+
outputs:
2753+
query:
2754+
rows:
2755+
- col_a: 1
2756+
""",
2757+
encoding="utf-8",
2758+
)
2759+
2760+
mock = _patch_state_access(mocker)
2761+
args = ["--gateway", "memory", "--paths", str(repo_2), "test"]
2762+
2763+
# Without --local the same run reaches the state backend.
2764+
runner.invoke(cli, args)
2765+
assert mock.called, "state-sync was never accessed during `test`"
2766+
2767+
mock.reset_mock()
2768+
2769+
result = runner.invoke(cli, [*args, "--local"])
2770+
2771+
assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}"
2772+
# Console output wraps, so compare against whitespace-normalized text.
2773+
output = " ".join(result.output.split())
2774+
assert 'Model \'"memory"."bronze"."a"\' was not found' in output, (
2775+
"the unloaded model should warn rather than fail"
2776+
)
2777+
assert "Successfully Ran 1 tests" in output, "the repo_2 test should still run"
2778+
mock.assert_not_called()

0 commit comments

Comments
 (0)