Add live-server integration coverage for the #40/#41 query-helper relaxations - #48
Add live-server integration coverage for the #40/#41 query-helper relaxations#48craigmcchesney wants to merge 2 commits into
Conversation
PR #47 verified both server behaviors by reading the dp-service source, but shipped unit tests only. A unit test asserts the request the client builds -- `values == []`, `criteria == []` -- which is precisely the assertion that cannot tell a working server from a broken one: - #40: an attributes criterion with no values must become Filters.exists("attributes.<key>"). The plausible failure is an `$in: []`, which matches NOTHING while producing a request that looks identical on the wire. - #41: an omitted criteria list must be match-all. The plausible failure is a business error. Four tests, ten subtests, covering PV metadata, configurations, activations, and the v2 PvQuery.attr selector -- every helper #40 touched that a live server can reach, plus #41's browse-all on all three annotation families. The load-bearing shape is a PAIR of assertions: the key-only query must find the record, AND a query naming a value the record does not have must not. Alone, the first cannot distinguish an existence filter from a match-all, which is the whole thing being tested. Verified non-vacuous by temporarily reintroducing the old behavior and confirming the tests fail with the empty-result symptom, not by assuming a passing test proves anything. The v2 class ingests its own samples, since a query selector needs archived data to select, and polls for bucket visibility rather than sleeping a fixed interval -- ingestData() acks before the bucket is queryable. It goes through the generated stub because IngestionClient wraps only registerProvider() until #17, matching test_datasets_annotations_integration.py. Every record is namespaced by a per-run id and removed via addCleanup, so repeat runs do not accumulate state and a mid-test failure still tears down; the annotation catalogue is empty again after a run. Tests self-skip with an actionable message when the services are absent, and CI's `-m "not integration"` deselects all 47 integration tests, so this cannot affect the CI signal. 771 pass with the ecosystem up (725 unit + 46 integration); 725 pass and 47 deselect without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019he3UCsAnqTDE2VQ73Djwn
There was a problem hiding this comment.
🟡 Changes recommended
Multiple unresolved test reliability, isolation, cleanup, and error-handling issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds live-server integration coverage for the #40/#41 query-helper relaxations.
Changes:
- Tests key-only searches and browse-all behavior.
- Adds v2 selector ingestion and visibility polling.
- Updates ticket plans and
CLAUDE.md.
File summaries
| File | Summary |
|---|---|
tests/integration/test_query_helper_relaxations_integration.py |
Adds integration tests and fixtures; requires fixes for service checks, isolation, cleanup, setup failures, catalogue handling, and short-circuiting. |
plan/tickets/41/plan.md |
Records #41 integration coverage. |
plan/tickets/40/plan.md |
Records #40 integration coverage. |
CLAUDE.md |
Documents the integration coverage. |
Review details
Suppressed comments (8)
tests/integration/test_query_helper_relaxations_integration.py:318
- This second-level run id has the same collision window for the ingested PV and provider. Concurrent test processes can select or clean up each other's data, so the v2 selector test is not isolated despite the surrounding comments; use a UUID or at least
time.time_ns()here as well.
cls.run_id = int(time.time())
tests/integration/test_query_helper_relaxations_integration.py:335
unittestdoes not calltearDownClasswhensetUpClassraises, including theSkipTestpaths in_ingest_samples()and_catalogue_pv(). Therefore a setup failure after opening the ingestion channel can leave the channel and test-side state behind; registeraddClassCleanupas each resource is created instead of relying only on this teardown method.
def tearDownClass(cls):
# Guarded: an early skip can leave these unset.
if getattr(cls, "client", None) is not None:
cls.client.annotation.pv_metadata.delete_pv_metadata(cls.pv_name)
if getattr(cls, "_ingestion_channel", None) is not None:
cls._ingestion_channel.close()
tests/integration/test_query_helper_relaxations_integration.py:348
- The service-reachability check has already passed, so a registration business error is a test setup failure, not an unavailable-service condition. Raising
SkipTesthere skips the entire class and can make broken ingestion coverage appear green; reserve skips for a known unsupported backend and fail the setup instead.
if registration.HasField("exceptionalResult"):
raise unittest.SkipTest(
f"could not register an ingestion provider: {registration.exceptionalResult.message}"
tests/integration/test_query_helper_relaxations_integration.py:367
- At this point the ingestion service was reachable, so an
ingestDatabusiness error indicates that the fixture or server is broken. Converting it toSkipTestsilently removes the v2 selector test from the run instead of reporting a failure; this should fail setup rather than skip.
response = stub.ingestData(request, timeout=15)
if response.HasField("exceptionalResult"):
raise unittest.SkipTest(f"could not ingest test data: {response.exceptionalResult.message}")
tests/integration/test_query_helper_relaxations_integration.py:381
- A reachable annotation service returning an error from
save_pv_metadatais not an absent-service condition. Skipping the class here can hide a real catalogue/API regression and report no v2 coverage; treat this fixture setup failure as a test failure, reservingSkipTestfor a deliberately detected incompatible backend.
if result.result_status.is_error:
raise unittest.SkipTest(f"could not catalogue the test PV: {result.result_status.message}")
tests/integration/test_query_helper_relaxations_integration.py:156
- When the catalogue exceeds 5,001 records, this path calls
skipTestinstead of checking whether the saved PV is reachable. That makes the key #41 integration assertion disappear in larger environments, so a browse-all regression can go unnoticed; iterate until the unique record is found and fail if it is absent rather than skipping.
if count >= 5000:
self.skipTest("catalogue too large to confirm browse-all reachability within the scan bound")
tests/integration/test_query_helper_relaxations_integration.py:213
- This materializes every configuration returned by the unfiltered iterator just to test for one run-unique record. On a large live catalogue it needlessly increases runtime and memory; use a short-circuiting
any(...)or an explicit loop that stops once the target is found.
self.assertIn(
config_name,
[c.configurationName for c in mc.iter_configurations()],
"iter_configurations() with no criteria should reach the saved configuration",
)
tests/integration/test_query_helper_relaxations_integration.py:292
- This also materializes the complete unfiltered activation catalogue even though the test needs only the run-unique activation id. A short-circuiting
any(...)or early-break loop avoids unnecessary runtime and memory on a populated live service.
self.assertIn(
activation_id,
[a.clientActivationId for a in mc.iter_configuration_activations()],
"iter_configuration_activations() with no criteria should reach the saved activation",
)
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _require_service(cls.logger, "annotation", ANNOTATION_ADDRESS) | ||
|
|
||
| cls.client = MldpClient() | ||
| cls.run_id = int(time.time()) |
| for label, address in (("annotation", ANNOTATION_ADDRESS), ("ingestion", INGESTION_ADDRESS)): | ||
| _require_service(cls.logger, label, address) |
| response = stub.ingestData(request, timeout=15) | ||
| if response.HasField("exceptionalResult"): | ||
| raise unittest.SkipTest(f"could not ingest test data: {response.exceptionalResult.message}") |
Four fixes from the PR #48 review (three of them Copilot's findings): - Probe the query service in TestV2SelectorRelaxations. _query_columns() calls client.query.query_samples() against localhost:50052, but setUpClass probed only annotation and ingestion, so a down query service failed at the RPC instead of self-skipping as the module docstring promises. Also guards MldpClient.query being None rather than letting it surface as an AttributeError. - Use a millisecond run id (str(int(time.time() * 1000))), matching test_datasets_annotations_integration.py and test_sample_status_client_integration.py. A whole-second id is not a per-run namespace: two runners started in the same second shared every record name. - Make the v2 negative assertion's precondition explicit. An empty result means both "the selector matched nothing" and "the bucket is not queryable yet", and the negative assertion reads it as the first. Bucket visibility is now established up front with a name-list selector -- one that does not depend on the behavior under test -- so the ordering is no longer load-bearing. Verified non-vacuous: suppressing the ingest makes the guard fail with its own diagnostic rather than the negative case passing for the wrong reason. - Correct the docstring's cleanup claim. Catalogue records are torn down per run, but the ingested samples cannot be: the archive has no delete RPC, so each run leaves SAMPLE_COUNT samples under a run-unique PV name, the same residue test_datasets_annotations_integration.py leaves. CLAUDE.md's entry gets the same correction. 771 pass with the ecosystem up (725 unit + 46 integration), ruff clean, 107 cookbook snippets checked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019he3UCsAnqTDE2VQ73Djwn
Closes the gap left by #47: both relaxations were verified by reading the dp-service source, but shipped with unit tests only.
Why unit tests were not enough
A unit test asserts the request the client builds —
values == []for #40,criteria == []for #41. That is exactly the assertion that cannot distinguish a working server from a broken one:Filters.exists("attributes.<key>"). The plausible failure is an$in: [], which matches nothing while producing a request that looks identical on the wire.What's covered
Four tests, ten subtests:
PvQuery.attrselectorEach test asserts a pair: the key-only query must find the record, and a query naming a value the record does not have must not. Alone, the first assertion cannot tell an existence filter from a match-all — which is the whole thing under test.
Verified non-vacuous
I did not assume a passing test proves anything. I temporarily reintroduced the old behavior (empty values sent as a non-matching
$in) and confirmed the tests fail with the empty-result symptom:Then reverted and re-ran green.
Mechanics
The v2 class ingests its own samples — a query selector needs archived data to select — and polls for bucket visibility rather than sleeping, since
ingestData()acks before the bucket is queryable. It goes through the generated stub becauseIngestionClientwraps onlyregisterProvider()until #17, matchingtest_datasets_annotations_integration.py.Every record is namespaced by a per-run id and removed via
addCleanup, so repeat runs do not accumulate state and a mid-test failure still tears down; I confirmed the annotation catalogue is empty again after a run. Tests self-skip with an actionable message when the services are absent.No CI impact:
-m "not integration"deselects all 47 integration tests. 771 pass with the ecosystem up (725 unit + 46 integration); 725 pass and 47 deselect without it. Ruff clean, 107 cookbook snippets checked.Both plans get a dated note recording the added coverage, per
plan/README.md's rule that merged plans take correction notes rather than rewrites.Relates to #40, #41.
🤖 Generated with Claude Code
https://claude.ai/code/session_019he3UCsAnqTDE2VQ73Djwn