mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
## What First test coverage for the dashboard **panel** endpoints and the **dashboard search path**, running CTE/subquery `match_all` queries as real dashboard panels. Nothing in the repo previously touched either: `grep -rn "/panels\|search_type=dashboards" tests/api-testing/` returned zero hits. ## Why Panels query `_search?type=logs&search_type=dashboards&dashboard_id=…` — a different route into the engine than the Logs page (`search_type=ui`). The `match_all` + FST projection rewrite fixed in #13808 runs inside it, so a query that passes via Logs can still regress on a dashboard. The rewrite has to preserve the projection of the block the predicate sits in. When that block is the whole statement, the projection is obvious. When it's a CTE body feeding a JOIN, a window, or a four-branch UNION ALL, it has to preserve the *CTE's* output schema while the outer query reads different columns from it — which is where it went wrong. ## Coverage **71 distinct queries across 29 combination families**, all with `match_all` inside a `WITH …` body or a derived table: | Axis | Variants | |---|---| | Nesting depth | 1, 2, 3, and 5-level CTE chains | | Nesting kind | CTEs and derived tables (and one query using both) | | UNION ALL | 2, 3, and 4 separate `match_all` branches | | JOIN | CTE⋈CTE, derived⋈derived, cross-stream | | Window | `ROW_NUMBER`, `RANK`, `DENSE_RANK`, partitioned aggregates | | Negation | `NOT match_all`, and mixed with positive `match_all` | | Combined with | `COUNT(DISTINCT)`, `CASE`, `HAVING`, `histogram()` | Each case creates a real panel, runs it via the dashboard search path, then asserts row count against the DuckDB oracle and that no expected column was dropped from the projection. A further 30 cases run the same SQL through **both** Logs and dashboard and diff the results — that's what catches a rewrite firing on only one path. Queries are reused from the existing query-agent corpus rather than duplicated, so the two suites can't drift: query-agent verifies the values via the Logs path, this verifies the same SQL survives the dashboard path. Cases are **discovered by classifying the SQL**, not from a hand-written ID list, so new combinations added to the corpus are picked up automatically. Test ids name the shape (`Q1022-cte1_union_cdist_4ma`), so a failure identifies the combination without opening the corpus. Plus 14 tests for the panel endpoints themselves — AddPanel / UpdatePanel / DeletePanel, contract + CRUD lifecycle + 7 error paths derived from `DashboardError` in `src/core/src/http.rs`. ## Changes | File | | |---|---| | `tests/dashboards/test_dashboard_cte_panels.py` | new — 103 tests | | `tests/dashboards/test_dashboard_panels.py` | new — 14 tests | | `tests/dashboards/conftest.py` | new — shared fixtures | | `support/panel_queries.py` | new — discovery + panel helpers | | `support/endpoints/dashboards.py` | +78, additive | | `support/endpoints/search.py` | +36, additive | | `support/factories.py` | +73, additive | The three shared-file edits are **187 added lines, 0 modified** — nothing existing can break. ## Results 168 tests in tests/api-testing/tests/dashboards/ (117 new + 51 pre-existing) 166 passed, 1 skipped, 1 xfailed # full nested-query corpus present 74 passed, 2 skipped, 1 xfailed # current corpus on main Repeated 2× and 4× respectively, no flakes. Discovery scales with the corpus: on main it finds 8 nested `match_all` cases, and 71 once the larger sentinel set is present. Cases that aren't in the corpus skip with an explanatory message instead of failing, so this is safe to merge in either order.
OpenObserve API Tests
End-to-end pytest suite against the OpenObserve REST API. Runs in CI on every
PR (.github/workflows/api-testing.yml); also runnable locally.
Quick start
# one-time setup
brew install rye # or: curl -sSf https://rye.astral.sh/get | bash
rye sync # install Python + deps from requirements.lock
cargo build # build the OO debug binary (~10 min first time)
# every run
ZO_ROOT_USER_EMAIL=root@example.com \
ZO_ROOT_USER_PASSWORD='Complexpass#123' \
ZO_BASE_URL=http://localhost:5080/ \
WS_ZO_BASE_URL=ws://localhost:5080/ \
target/debug/openobserve &
rye run pytest # full suite
rye run pytest -m framework # just the new-framework self-tests
rye run pytest tests/test_kvstore.py -v # one file
Layout
tests/api-testing/
├── pyproject.toml # pytest + ruff config
├── conftest.py # legacy session-wide fixtures (do not extend)
├── support/ # NEW FRAMEWORK — write new tests against this
│ ├── client.py # OpenObserveClient: HTTP entry point
│ ├── endpoints/ # one wrapper class per resource group
│ │ ├── search.py # client.search.sql(...), .hits(...), .count(...)
│ │ ├── streams.py # client.streams.list(), .delete(...), .ingest_json(...)
│ │ ├── users.py
│ │ ├── dashboards.py
│ │ ├── alerts.py
│ │ ├── folders.py
│ │ └── kvstore.py
│ ├── factories.py # payload builders (user_payload, dashboard_payload, …)
│ ├── wait.py # wait_until(predicate, timeout, ...) — REPLACES time.sleep
│ └── fixtures.py # client, temp_stream_name, temp_user_email, …
├── tests/ # test files
│ ├── conftest.py
│ ├── test_framework_selfcheck.py # 14 tests that prove support/ works
│ └── test_*.py # 40+ legacy test files (in revamp progress)
├── helpers/ # non-test shared code
│ ├── sourcemap_helpers.py
│ └── workflow/ # workflow chain helpers (with embedded pages/)
├── fixtures/ # static test data
│ └── rum/ # RUM ingest scripts
└── README.md # this file
Writing a new test — the rules
Use the new framework. Don't copy-paste from old test_*.py files; they
are scheduled for rewrite (see revamp plan).
# tests/<area>/test_my_feature.py
from support.client import OpenObserveClient
from support.factories import user_payload
from support.fixtures import client, temp_user_email # noqa: F401
from support.wait import wait_until
def test_create_user_returns_complete_record(client: OpenObserveClient, temp_user_email: str):
"""Create a user — verify the response has the email AND role we sent."""
payload = user_payload(email=temp_user_email, role="admin")
resp = client.users.create(payload)
assert resp.status_code == 200, resp.text
body = resp.json()
# VALIDATE THE BODY, not just status_code
assert body.get("data", {}).get("email") == temp_user_email
assert body.get("data", {}).get("role") == "admin"
# cleanup happens automatically via temp_user_email fixture
Mandatory patterns
| ✅ Do | ❌ Don't |
|---|---|
from support.fixtures import client |
session = create_session; url = base_url; org_id = "default" (307 occurrences in legacy code) |
client.streams.list() |
session.get(f"{url}api/{org_id}/streams") |
body = resp.json(); assert body["foo"] == ... |
assert resp.status_code == 200 and stop |
wait_until(lambda: client.search.count(sql) >= N, timeout=30) |
time.sleep(5) |
unique_email("user") / temp_user_email fixture |
hardcoded pytests@gmail.com |
logger.debug("got %s", resp.status_code) |
print(resp.content) |
start, end = time_window(minutes=15) |
hardcoded start_time = 1700629279639000 |
Anti-patterns that fail review
assert resp.status_code in [200, 400, 404, 500]— pick one. If the API behaves wrongly, file a bug; don't paper over it.pytest.skip(...)when the test discovers the bug it's meant to catch — use@pytest.mark.xfail(strict=True).- Commented-out test functions — delete them.
pages.*direct imports — that subtree is legacy. New tests usesupport.client.
Lint
rye run ruff check support/ tests/test_framework_selfcheck.py # strict mode
rye run ruff check --fix support/ # autofix
Existing legacy tests have a per-file ruff-ignore (so print etc. don't fail
the whole suite); new tests under support/ and any new well-formed test
file should pass strict ruff.
Test markers
| Marker | Meaning |
|---|---|
framework |
Self-tests for support/ framework. Run with -m framework. |
order(N) |
Run order pin (legacy — avoid in new tests). |
Troubleshooting
KeyError: 'ZO_BASE_URL'on collection —conftest.pyreads env vars at import. Export them before running pytest (even--collect-only).test_workflow.pyfails with SMTP error —ZO_SMTP_ENABLED=truenot set. CI does this; locally you'd need to wire an SMTP env.- Test_organisations.py warning about
yield—test_create_organizationuses pre-pytest-4 yield-style and is silently ignored. Scheduled for rewrite in Phase 4.
Revamp in progress
This suite is being incrementally reworked. See:
../../tests/ui-testing/MD_Files/api tests revamp/API_TESTS_REVAMP.md— the plan../../tests/ui-testing/MD_Files/api tests revamp/oss_rework_progress/— phase-by-phase progress