Skip to content

Commit 44ef41b

Browse files
test(query-engine): add differential PromQL testing infra (#590, #594) (#630)
* feat(promql-compliance): add remote-write data seeder (#594) Adds promql-compliance/seeder, a standalone Go module that builds a prompb.WriteRequest for a fixed, hand-authored dataset (counters, a gauge, and a label-churn series designed to exercise instant-vs-range divergence), snappy-compresses it, and pushes the same bytes via remote write to both a reference Prometheus and ASAPQuery's own remote-write ingest endpoint. This is the data-seeding half of the differential PromQL compliance harness proposed in #594; the comparison-harness half is a separate workstream. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(query-engine): add generic multi-step range/instant equivalence oracle (#590) Adds the test #590 calls out as missing: for a stable key set, one range(start,end,step) query's per-timestamp series must exactly equal N separate instant(t) queries (one per step), not just checked at a single timestamp. Covers {Tumbling, Sliding} x {single-population Sum, dual-population Count} x >=4 steps. All four pass against current code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(promql-compliance): add instant-query diffing to promql-compliance-tester Vendor prometheus/compliance/promql (commit 67b8327, Apache 2.0) into promql-compliance/harness/ and patch Comparer.Compare to also run and diff an instant query (PromAPI.Query) alongside the existing range query, since upstream only ever exercised QueryRange. Range and instant outcomes are tracked and reported independently (Result.RangeSuccess/InstantSuccess), so "PASS: range, FAIL: instant" is a representable outcome instead of one aggregate pass/fail -- this is the exact bug class cataloged in #589. Adds config.yaml with placeholder reference/test target URLs and four seed regression test cases ported from the query patterns described in #589, #583, and #584 (top-k over range, per-step key snapshot churn, and two sliding-window rate queries). Adds comparer_test.go with fake in-process PromAPI doubles covering both diverging directions plus error/should-fail handling, since no unit tests existed upstream. Part of #594. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(query-engine): add boundary/validation tests for range queries (#590) validate_range_query_params had no dedicated tests. Adds direct unit tests for its three error branches (start>=end, step==0, step not a multiple of the tumbling window) plus the happy path, in an inline test module next to the function (it's private and validate_range_query_params's error string is discarded before reaching any public caller, so the exact-string assertions can't be made from crate::tests). Also adds end-to-end coverage via handle_range_query_promql confirming each bad-param case is actually rejected in practice, including the start == end boundary specifically, and a new test that runs the same keys-but-no-value orphan-group scenario through both the instant and range entry points and diffs their skip/error behavior explicitly, so a future regression that splits their behavior fails here instead of only in one of the two existing per-path tests. * test(query-engine): keyed sliding-window property oracle (#590) Adds simulate_sliding_window_keyed, a pure-function sliding-window oracle extended to keyed/grouped data (a sibling to the existing simulate_sliding_window/simulate_sliding_window_with_alignment, kept separate since those two are exercised by several existing index-slicing call sites that don't have a notion of a key). Drives it against a small deterministic sweep of window/slide configs and per-key presence patterns (appearing, disappearing, oscillating, gap mid-range) through a real SimpleEngine via create_engine_multi_timestamp_with_window + handle_range_query_promql. The sweep surfaced a real bug: execute_range_query_pipeline computes each Sliding-window step's window_start as current_time.saturating_sub(lookback_ms), so every output step before window_size_ms worth of history exists aliases onto the store's start=0 window instead of correctly having no sample. Captured as a minimal, #[ignore]'d regression (sliding_window_range_query_start_before_window_size_ms_returns_wrong_value) rather than patched, per #590's ground rules; the main property sweep starts each scenario at its own window_size_ms to avoid that known-buggy region while still exercising the key-expansion/merge behavior it's meant to check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(promql-compliance): add end-to-end quick start * test(query-engine): add fixture-driven differential runner * test(query-engine): stabilize differential smoke run * test(query-engine): derive compatible differential planner timing * docs(query-engine): document differential experiment workflow * test(query-engine): align planner timing with query ranges * refactor(asap-tools): remove legacy promql harness * refactor(query-engine): move range tests to follow-up issue * fix(asap-tools): validate differential range resolution * fix(asap-tools): reject off-grid instant comparisons * ci(query-engine): cache differential Docker builds --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9fb051a commit 44ef41b

32 files changed

Lines changed: 3457 additions & 0 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: PromQL Differential Tests
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
branches: [main]
7+
paths:
8+
- 'promql-compliance/**'
9+
- 'asap-query-engine/**'
10+
- 'asap-planner-rs/**'
11+
- 'asap-common/**'
12+
- 'Cargo.toml'
13+
- 'Cargo.lock'
14+
- '.github/workflows/promql-differential.yml'
15+
workflow_dispatch:
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
permissions:
22+
contents: read
23+
packages: read
24+
25+
jobs:
26+
differential:
27+
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 45
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Set up Docker Buildx
34+
uses: docker/setup-buildx-action@v3
35+
36+
- name: Log in to GHCR for Docker layer cache
37+
uses: docker/login-action@v3
38+
with:
39+
registry: ghcr.io
40+
username: ${{ github.repository_owner }}
41+
password: ${{ secrets.GITHUB_TOKEN }}
42+
43+
- name: Set up Go
44+
uses: actions/setup-go@v5
45+
with:
46+
go-version: '1.25.x'
47+
48+
- name: Run seeder tests
49+
working-directory: promql-compliance/seeder
50+
run: go test ./...
51+
52+
- name: Run runner tests
53+
working-directory: promql-compliance/runner
54+
run: go test ./...
55+
56+
- name: Run live PromQL differential suite
57+
working-directory: promql-compliance/runner
58+
run: make run
59+
60+
- name: Upload differential report and service logs
61+
if: always()
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: promql-differential-results
65+
path: |
66+
promql-compliance/runner/differential-report.json
67+
/tmp/asapquery-differential-*
68+
if-no-files-found: warn

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ asap-quickstart/bin/
55
# roborev snapshots
66
/.roborev/
77

8+
# Differential test output
9+
promql-compliance/runner/differential-report.json
810
/.agent/
911

1012
/**/*.log

promql-compliance/ARCHITECTURE.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Differential experiment architecture
2+
3+
The differential runner answers one question:
4+
5+
> Given the same samples and the same PromQL request, does ASAPQuery produce
6+
> the same result as Prometheus?
7+
8+
The experiment uses Prometheus as the reference implementation. ASAPQuery is
9+
the system under test.
10+
11+
## Stack
12+
13+
```text
14+
query requests
15+
┌─────────────────────┐
16+
│ differential runner│
17+
└─────────┬───────────┘
18+
19+
┌─────────────┴─────────────┐
20+
│ │
21+
Prometheus reference ASAPQuery query API
22+
host :19090 host :18088
23+
▲ ▲
24+
│ │
25+
remote-write data remote-write ingest
26+
│ host :19091
27+
│ ▲
28+
└───────────┬───────────────┘
29+
30+
identical samples
31+
32+
ASAPQuery planner ──► shared planner-output volume ──► query engine
33+
```
34+
35+
The Compose services are:
36+
37+
| Service | Role | Host ports |
38+
| --- | --- | --- |
39+
| `prometheus` | Reference PromQL implementation and backend | `19090` |
40+
| `planner` | Generates ASAPQuery inference and streaming configuration | none |
41+
| `queryengine` | Runs ASAPQuery in precompute mode | query `18088`, ingest `19091` |
42+
43+
Inside the Compose network, the query engine reaches Prometheus at
44+
`http://prometheus:9090`. The engine configuration sets
45+
`forward_unsupported_queries: false`, so an unsupported query is rejected
46+
instead of being answered by Prometheus.
47+
48+
## Run lifecycle
49+
50+
The runner performs these stages:
51+
52+
1. Load a dataset fixture and a query suite.
53+
2. Choose a base timestamp. Dataset sample offsets and suite evaluation times
54+
are relative to this timestamp.
55+
3. Generate temporary planner and engine configuration.
56+
4. Start Prometheus and wait for its health check.
57+
5. Run the planner. Its output is placed in a named shared volume.
58+
6. Start the ASAPQuery query engine after the planner completes.
59+
7. Send the same encoded remote-write batches to Prometheus and ASAPQuery.
60+
8. Wait for both query APIs to be ready and for the first probe query to return
61+
samples.
62+
9. Execute every configured range and instant query against both targets.
63+
10. Write the JSON report and, by default, remove the containers, network, and
64+
named volumes.
65+
66+
Use `--keep-services` when inspecting logs or making manual requests after the
67+
runner exits.
68+
69+
## Configuration boundaries
70+
71+
The checked-in dataset and suite are inputs to the runner, not service
72+
configuration. The runner generates these temporary files:
73+
74+
- `controller-config.yaml`: metrics, query groups, planner timing, and cleanup
75+
policy.
76+
- `engine_config.yaml`: HTTP ports, backend, ingestion, logging, and paths to
77+
planner output.
78+
79+
The planner receives metric and label hints from the dataset, so it does not
80+
need to discover them from a separate live data source in this workflow.
81+
82+
## Comparison model
83+
84+
For each query, the runner can perform four related checks:
85+
86+
- Range comparison: Prometheus range result versus ASAPQuery range result.
87+
- Instant comparison: Prometheus instant result versus ASAPQuery instant result.
88+
- Reference parity: Prometheus range-at-t versus Prometheus instant-at-t.
89+
- Test parity: ASAPQuery range-at-t versus ASAPQuery instant-at-t.
90+
91+
The report passes only if every configured comparison passes. An unexpected
92+
HTTP/query error from either target fails the comparison, even if both targets
93+
fail. The exception is a query explicitly marked `expect_error: true`, where
94+
both targets must return an error.
95+
96+
Equal successful empty results are still equal results; use a dataset and
97+
probe query that should contain samples when testing ingestion readiness.
98+
99+
## Current limitations
100+
101+
The runner’s readiness check currently probes the first suite query and its
102+
first evaluation time. It does not yet expose a general ingestion watermark or
103+
drain signal for proving that every asynchronous batch has finished processing.
104+
For the same reason, suites should currently use a supported, non-empty first
105+
query as their readiness probe. These are tracked as follow-up synchronization
106+
work.

0 commit comments

Comments
 (0)