Skip to content

Add live-server integration coverage for the #40/#41 query-helper relaxations - #48

Open
craigmcchesney wants to merge 2 commits into
mainfrom
test/issue-40-41-integration-coverage
Open

Add live-server integration coverage for the #40/#41 query-helper relaxations#48
craigmcchesney wants to merge 2 commits into
mainfrom
test/issue-40-41-integration-coverage

Conversation

@craigmcchesney

Copy link
Copy Markdown
Collaborator

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:

What's covered

Four tests, ten subtests:

Path #40 key-only #41 browse-all
PV metadata
Configurations
Configuration activations
v2 PvQuery.attr selector n/a — deliberately out of scope

Each 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:

AssertionError: 'ITEST:RELAX:PV:...' not found in [] :
  key-only search (values empty list) must match on key existence alone

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 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; 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

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
Copilot AI lite review requested due to automatic review settings September 10, 2026 21:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  • unittest does not call tearDownClass when setUpClass raises, including the SkipTest paths in _ingest_samples() and _catalogue_pv(). Therefore a setup failure after opening the ingestion channel can leave the channel and test-side state behind; register addClassCleanup as 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 SkipTest here 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 ingestData business error indicates that the fixture or server is broken. Converting it to SkipTest silently 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_metadata is 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, reserving SkipTest for 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 skipTest instead 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())
Comment on lines +314 to +315
for label, address in (("annotation", ANNOTATION_ADDRESS), ("ingestion", INGESTION_ADDRESS)):
_require_service(cls.logger, label, address)
Comment on lines +365 to +367
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants