[feature][testing-harness] Add TestHarness Phase 3: end-to-end run against real Glean#63
[feature][testing-harness] Add TestHarness Phase 3: end-to-end run against real Glean#63dishant-gupta-glean wants to merge 15 commits into
Conversation
…esting Introduces glean.indexing.testing.harness with TestConfig (YAML-driven config) and TestHarness exposing run_full_mock / run_full_mock_async. Phase 1 delegates to the existing run_connector / run_connector_async helpers — no new mock infrastructure, full backward compatibility. Phase 2 (integration) and Phase 3 (end-to-end) method stubs raise NotImplementedError until subsequent PRs land. TestHarness and TestConfig are also re-exported from glean.indexing.testing so existing imports are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces recording/replay data-client wrappers (sync, streaming, async) and CacheManifest (versioned fixture metadata as NDJSON + JSON manifest). TestHarness.run_integration_test / run_integration_test_async now: - Monkey-patches each named connector attribute with a recording wrapper on cache miss, or a replay wrapper on cache hit (matched by SDK version) - Restores the original client after the run in all cases - Respects max_items per-client config and refresh_cache flag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions Adds permissions.py with extract_permission_refs (collects all user/group identities from DocumentDefinition ACL fields) and assert_negative_identities_absent (raises AssertionError if any listed identity appears in any document's allowedUsers, allowedGroups, or allowedGroupIntersections). run_integration_test and run_integration_test_async now run the assertion automatically when config.negative_test_identities is non-empty, covering email, datasource_user_id, and group intersection paths. PermissionRefs, extract_permission_refs, and assert_negative_identities_absent are exported from glean.indexing.testing.harness for advanced usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add run_end_to_end and run_end_to_end_async to TestHarness, completing the three-phase testing model. Phase 3 calls connector.index_data() (or index_data_async for async connectors) against the real Glean API with per-client max_items applied via _patched_clients. Removes the now-stale Phase 3 NotImplementedError stubs from test_harness_phase1.py and adds test_harness_phase3.py with 7 unit tests that use patch.object to validate wiring without requiring live Glean credentials. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| # Make it awaitable | ||
| import asyncio | ||
|
|
||
| mock_async.return_value = asyncio.coroutine(lambda: None)() |
There was a problem hiding this comment.
Issue: The async unit test test_async_connector_calls_index_data_async uses asyncio.coroutine(lambda: None)(), but asyncio.coroutine is deprecated and removed in Python 3.11+, which will cause an AttributeError and break tests on modern Python versions.
Suggested fix: Replace the use of asyncio.coroutine with a real async function or an already-resolved awaitable. For example:
async def _noop():
return None
mock_async.return_value = _noop()Alternatively, use unittest.mock.AsyncMock for index_data_async so that the mock is natively awaitable.
🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean
💬 Help us improve! Was this comment helpful? React with 👍 or 👎
|
|
||
| Requires ``GLEAN_SERVER_URL`` (or ``GLEAN_INSTANCE``) and | ||
| ``GLEAN_INDEXING_API_TOKEN`` environment variables to be set. | ||
| Calls ``connector.index_data()`` without any mock patching. Per-client |
There was a problem hiding this comment.
Issue: The Phase 3 documentation for TestHarness.run_end_to_end states that it "Calls connector.index_data() without any mock patching", but the implementation wraps the call in _patched_clients(self._connector, self._clients, self._config), and tests assert that the connector's client is replaced during the run, so the docstring is misleading about patching behavior.
Suggested fix: Update the Phase 3 and run_end_to_end docstrings to clarify that while no network mocking of the Glean server is performed, connector clients are temporarily wrapped/patched (e.g., to enforce max_items). For example, rephrase to: "Calls connector.index_data() against a real Glean instance without mocking the Glean API; connector clients are temporarily wrapped to apply per-client max_items limits."
🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean
💬 Help us improve! Was this comment helpful? React with 👍 or 👎
There was a problem hiding this comment.
Pull request overview
Adds Phase 3 of the TestHarness to run a connector end-to-end against a real Glean instance (using environment-configured credentials), while still applying the existing max_items safety bound via the same client-patching mechanism used in Phase 2.
Changes:
- Implement
TestHarness.run_end_to_end()to patch configured clients (formax_items) and invokeconnector.index_data()against real Glean. - Implement
TestHarness.run_end_to_end_async()to callindex_data_async()for async-streaming connectors and fall back toindex_data()for sync connectors, with the same client patching. - Add new unit tests for Phase 3 behavior and remove Phase 3 “NotImplemented” stub assertions from Phase 1 tests.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/glean/indexing/testing/harness/harness.py |
Implements Phase 3 sync/async entrypoints and updates Phase 3 documentation. |
tests/unit_tests/testing/harness/test_harness_phase3.py |
Adds unit tests validating Phase 3 method dispatch and client patch/restore behavior. |
tests/unit_tests/testing/harness/test_harness_phase1.py |
Removes tests asserting Phase 3 methods raise NotImplementedError. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Unit tests here validate the method interface and max_items wiring without | ||
| making actual network calls. The end-to-end flow is covered by integration | ||
| tests (marked ``@pytest.mark.integration``) that require | ||
| ``GLEAN_INDEXING_API_TOKEN`` in the environment and are not run in CI by | ||
| default. |
| # Patch index_data so we don't need a real Glean server, but still | ||
| # let the client patching happen | ||
| original_index_data = connector.index_data | ||
|
|
||
| def _capture_and_call(mode, options): | ||
| # Verify the client is wrapped (not the original) during index_data | ||
| assert connector.data_client is not real_client | ||
| # Still call original so client gets exercised | ||
| original_index_data(mode=mode, options=options) | ||
|
|
||
| with patch.object(connector, "index_data", side_effect=_capture_and_call): | ||
| # This will fail at PushUploader (no creds), so just check AttributeError | ||
| # is not raised first — the wrapping is what we're testing | ||
| try: | ||
| harness.run_end_to_end() | ||
| except Exception: | ||
| pass # Expected — no Glean credentials in unit test env |
| with patch.object(connector, "index_data_async") as mock_async: | ||
| mock_async.return_value = None | ||
| # Make it awaitable | ||
| import asyncio | ||
|
|
||
| mock_async.return_value = asyncio.coroutine(lambda: None)() | ||
| await harness.run_end_to_end_async() | ||
| mock_async.assert_called_once_with(mode=IndexingMode.FULL, options=None) |
| Calls ``connector.index_data()`` without any mock patching. Per-client | ||
| ``max_items`` from :attr:`config` are applied to bound the crawl. |
| Raises: | ||
| NotImplementedError: Always, until PR 4 is merged. | ||
| RuntimeError: If ``GLEAN_INDEXING_API_TOKEN`` or the server URL is | ||
| missing from the environment (raised by the underlying | ||
| :mod:`~glean.indexing.common.glean_client` module). |
| Raises: | ||
| NotImplementedError: Always, until PR 4 is merged. | ||
| RuntimeError: If environment variables for the Glean client are missing. |
- config.py: treat `testing: null` in YAML as {} instead of crashing
- harness.py: clarify Phase 1 doc — only Glean side is mocked, not source
- harness.py: add public `clients` property (returns a copy of _clients)
- test_harness_phase1.py: assert on harness.clients instead of _clients
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- replay_client.py: add max_items param to all three replay wrappers so record and replay are capped consistently - harness.py: pass max_items to replay wrappers in _wrap_client - harness.py: check data.ndjson exists alongside manifest before cache hit to avoid hard failures on partial writes - harness.py: move patching loop inside try/finally so partial patches are always reverted on _wrap_client exception - recording_client.py: remove misleading "atomically" from module docstring - test_harness_phase2.py: derive _SDK_VER dynamically via importlib.metadata Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rness-permissions
- permissions.py: clarify docstring — email is preferred over datasource_user_id (not "whichever is set") - permissions.py: document allowedGroupIntersections[*].requiredGroups coverage in assert_negative_identities_absent docstring - testing/__init__.py: re-export PermissionRefs, extract_permission_refs, assert_negative_identities_absent from glean.indexing.testing top-level Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng-harness-phase3
- harness.py: clarify run_end_to_end docstring — Glean API is not mocked but clients registered via 'clients' arg are still wrapped for max_items; document MissingEnvironmentVariableError instead of RuntimeError for both sync and async variants - test_harness_phase3.py: fix module docstring — no @pytest.mark.integration tests exist; live runs are manual - test_harness_phase3.py: replace bare except-all in test_max_items_applied_to_client with a sentinel exception so assertion failures inside the side_effect are not accidentally swallowed - test_harness_phase3.py: replace asyncio.coroutine (removed in Python 3.11) with AsyncMock; use assert_awaited_once_with Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rness-permissions
…ng-harness-phase3
Problem
Phase 1 and Phase 2 validate connector logic without touching Glean's API. Before shipping a connector, teams need a way to do a bounded end-to-end smoke test — real source data, real Glean indexing — with a safety dial to limit how many items get crawled so they don't flood production.
Solution
Implement `run_end_to_end` and `run_end_to_end_async` as the final phase of the `TestHarness` testing model. Both methods:
`run_end_to_end_async` dispatches to `connector.index_data_async()` for `BaseAsyncStreamingDatasourceConnector` subclasses and falls back to `connector.index_data()` for sync connectors.
TestHarness additions
File layout
Unit tests use `patch.object` to mock `index_data` / `index_data_async` so they can run in CI without credentials. They verify:
Validation
uv run pytest tests/unit_tests/testing/harness/ -v # 95 tests, all pass uv run ruff check src/glean/indexing/testing/harness/ tests/unit_tests/testing/harness/Notes