Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ on:
branches: [ main, dev ]
workflow_dispatch: # Enables manual triggering
schedule:
- cron: '0 0 2 * *' # Runs at 00:00 UTC on the 2nd day of every month
- cron: '0 0 * * 0' # Runs at 00:00 UTC every Sunday (weekly data-health audit)

# One live run per branch. Every run drives the full correctness suite against
# the production VFB backend (Neo4j / SOLR / Owlery), so a series of quick
# the production data sources (VFB's Neo4j / SOLR / Owlery and FlyBase Chado), so a series of quick
# pushes to a PR would otherwise stack several full suites against production
# simultaneously. Superseded runs are cancelled — only the newest commit's
# result is meaningful anyway.
Expand Down Expand Up @@ -75,9 +75,19 @@ jobs:
# on today's GitHub-hosted ubuntu runner, so this is not a slowdown —
# it just stops the load on VFB infra changing silently if the hosted
# runner spec grows.
#
# data_health: the '*_live' term_info variants assert on the external
# indexer's built vfb_json content, not this repo's render code, so
# an incomplete/stale production document fails them. They must NOT gate
# code PRs — the PR-blocking versions are fixture-based and deterministic.
# So they run ONLY on the schedule (a data-health audit); on push /
# pull_request they are deselected. See TESTING.md.
MARK=(-m "not data_health")
[ "${{ github.event_name }}" = "schedule" ] && MARK=()
pytest -v -ra -n 4 --dist loadscope \
--ignore=src/test/test_query_performance.py \
--ignore=src/test/test_example_queries.py \
"${MARK[@]}" \
src/test tests 2>&1 | tee pytest_output.log
- name: Flag skipped tests (backend unavailable)
if: always()
Expand Down Expand Up @@ -105,7 +115,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
fi
if [ -n "$summary" ]; then
echo "::warning title=Tests skipped — VFB backend unreachable::${summary}. ${digest:+First failure: ${digest}. }These are NOT test failures and not a problem with this branch: the VFB backend (Neo4j / SOLR / Owlery) did not answer, so those queries went unverified this run. Treat a green check with skips as an incomplete run — re-run once the backend is healthy before relying on it. Full detail (every skipped test with its reason; failing URLs and probe verdicts for backend failures): the job summary and the PR comment."
echo "::warning title=Tests skipped — upstream data source unreachable::${summary}. ${digest:+First failure: ${digest}. }These are NOT test failures and not a problem with this branch: an upstream data source (VFB's Neo4j / SOLR / Owlery, or FlyBase's Chado) did not answer, so those queries went unverified this run. Treat a green check with skips as an incomplete run — re-run once the data sources are healthy before relying on it. Full detail (every skipped test with its reason; failing URLs and probe verdicts for backend failures): the job summary and the PR comment."
else
echo "No tests skipped."
fi
Expand Down Expand Up @@ -151,7 +161,7 @@ jobs:
// re-running it would do nothing.
const runUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${context.runId}`;
const rerun = `To re-run: open [this workflow run](${runUrl}) and click ` +
`**Re-run all jobs** once the backend is healthy (re-running the ` +
`**Re-run all jobs** once the data sources are healthy (re-running the ` +
`“Run completeness” check itself does nothing — it has no job behind it).`;

// A neutral (grey) status check so the PR's checks box stops reading
Expand All @@ -168,16 +178,16 @@ jobs:
details_url: runUrl,
output: {
title: skipped > 0
? `${skipped} test(s) skipped — backend unreachable (incomplete run)`
? `${skipped} test(s) skipped — upstream data source unreachable (incomplete run)`
: 'All backend tests ran',
summary: skipped > 0
? (`**${skipped}** test(s) were skipped because the VFB backend ` +
`(Neo4j / SOLR / Owlery) did not answer, so those queries went ` +
? (`**${skipped}** test(s) were skipped because an upstream data source ` +
`(VFB's Neo4j / SOLR / Owlery, or FlyBase's Chado) did not answer, so those queries went ` +
`unverified. This is not a branch failure — but the run is ` +
`incomplete.\n\n${rerun}` +
(summary ? '\n\n```\n' + summary + '\n```' : '') +
(outage ? '\n\n' + outage : ''))
: 'Every backend-dependent test reached the VFB backend and ran.',
: 'Every backend-dependent test reached its data source and ran.',
},
});
const comments = await github.paginate(github.rest.issues.listComments,
Expand All @@ -187,10 +197,10 @@ jobs:
if (skipped > 0) {
const body = [
marker,
`### ⚠️ ${skipped} test(s) skipped — VFB backend was unreachable`,
`### ⚠️ ${skipped} test(s) skipped — an upstream data source was unreachable`,
'',
`The full suite ran, but **${skipped}** test(s) were **skipped** because the ` +
`VFB backend (Neo4j / SOLR / Owlery) did not answer during this run.`,
`The full suite ran, but **${skipped}** test(s) were **skipped** because ` +
`an upstream data source (VFB's Neo4j / SOLR / Owlery, or FlyBase's Chado) did not answer during this run.`,
'',
'These are **not failures** and **not a problem with this branch** — but those ' +
'queries went **unverified**, so a green check here is an *incomplete* run.',
Expand Down
47 changes: 45 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ Most VFBquery tests run **live queries against the production VFB backend**
(SOLR, Neo4j, Owlery, FlyBase Chado). That makes them powerful — they catch real
regressions in query results — but also easy to write badly: a test that never
checks its query returned anything passes forever while the query is silently
broken. A whole class of such tests was found and fixed in Aug 2026; this doc
exists so they don't come back.
broken. This doc exists so that class of test does not creep back in.

Read this before adding or changing a test.

Expand Down Expand Up @@ -69,6 +68,50 @@ So an empty result is **never** an acceptable outcome for a known-populated
term — it is a bug. A backend outage is handled *for you*; you do not need
(and must not add) your own try/except to survive it.

## Fixture vs live (`data_health`): what an empty result actually means

The "empty ⇒ bug" rule assumes empty means **our** code is wrong. That holds
when a test reads the **source of truth (pdb / Neo4j) through this repo's own
query code** — connectivity, the neurotransmitter queries, and the like. There,
an empty result *is* a defect here, so the test asserts real content and is
**PR-blocking**.

It does **not** hold for **term_info** tests. `get_term_info` / `process` /
`term_info_parse_object` *render* a pre-built SOLR `vfb_json` document; that
document is built by a **separate repository** (`VFB_json_schema_indexer`), not
here. If that built document is stale or incomplete (e.g. a pub with no title, a
split class missing its `Expression_pattern` type), our render code faithfully
produces an empty/partial result — which is a problem with the **build**, not
with this repo. Gating a code PR on it is a false signal.

**The rule of thumb:** *if the build code is in this repo, test it here against
the source (empty = fail, PR-blocking); if the build is in another repo, use a
fixture here and test the build at its source.* Connectivity/NT queries build
from pdb here → tested here. term_info documents are built by
`VFB_json_schema_indexer` → **fixtures here, build tested there.**

So term_info tests come in two variants:

| Variant | Reads | Runs | A failure means |
|---|---|---|---|
| **fixture** (plain name) | a committed complete `vfb_json` doc in `src/test/fixtures/term_info/` | **every PR** (blocking) | this repo's **render code** regressed |
| **live** (`<name>_live`, `@pytest.mark.data_health`) | the live SOLR `vfb_json` document | **schedule only** | the **built document / pipeline** (`VFB_json_schema_indexer`) is wrong — a data-health signal, not a code gate |

Mechanics:

* Fixtures are captured from the PDB (the source of truth) — i.e. what the
indexer *should* produce — and verified to render the asserted output. They
make the code assertion **deterministic and independent of the live SOLR
`vfb_json` document and the caches**, so code improvements are never blocked by
stale data.
* The live variant carries `@pytest.mark.data_health`. PRs run
`-m 'not data_health'`; the weekly `schedule` in `python-test.yml` runs the
full set. A red scheduled `data_health` run means production `vfb_json` (or the
indexer that builds it) needs attention — it does **not** block merges.
* Do **not** paper over an empty term_info doc with a skip guard — that violates
rule 2. Use the fixture for the code assertion; let the `_live` variant fail on
the schedule, where empty is exactly the signal you want.

## The rules

### 1. Assert content, not just shape
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ build-backend = "setuptools.build_meta"
# blocks in performance.md — pure noise.
markers = [
"integration: integration tests that exercise the live VFB upstream (Neo4j, SOLR, owlery). Skip via -m 'not integration'.",
"data_health: live SOLR content-completeness checks (the '*_live' variants of the term_info tests). These assert on the external indexer's built 'vfb_json' documents, not this repo's render code, so an empty/incomplete production document fails them. Run on the schedule; deselected on PRs via -m 'not data_health'. See TESTING.md.",
]

# Ceiling on any single test. The upstream Neo4j intermittently stops answering
Expand Down
162 changes: 162 additions & 0 deletions src/test/fixtures/term_info/pub_FBrf0242477.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
{
"term": {
"core": {
"iri": "http://flybase.org/reports/FBrf0242477",
"symbol": "",
"types": [
"Entity",
"Individual",
"pub"
],
"short_form": "FBrf0242477",
"unique_facets": [
"pub"
],
"label": "Dolan et al., 2019, eLife 8: e43079"
},
"description": [],
"comment": []
},
"query": "Get JSON for pub",
"version": "0dcd736",
"dataset_license": [
{
"license": {
"core": {
"iri": "http://virtualflybrain.org/reports/VFBlicense_CC_BY_4_0",
"symbol": "CC_BY",
"types": [
"Entity",
"Individual",
"License"
],
"short_form": "VFBlicense_CC_BY_4_0",
"unique_facets": [],
"label": "CC-BY_4.0"
},
"icon": "http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by.png",
"link": "https://creativecommons.org/licenses/by/4.0/legalcode"
},
"dataset": {
"core": {
"iri": "http://virtualflybrain.org/reports/LateralHorn2019",
"symbol": "",
"types": [
"Entity",
"Individual",
"DataSet"
],
"short_form": "LateralHorn2019",
"unique_facets": [],
"label": "FlyLight split-GAL4 lines for Lateral Horn"
},
"link": ""
}
},
{
"license": {
"core": {
"iri": "http://virtualflybrain.org/reports/VFBlicense_CC_BY_4_0",
"symbol": "CC_BY",
"types": [
"Entity",
"Individual",
"License"
],
"short_form": "VFBlicense_CC_BY_4_0",
"unique_facets": [],
"label": "CC-BY_4.0"
},
"icon": "http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by.png",
"link": "https://creativecommons.org/licenses/by/4.0/legalcode"
},
"dataset": {
"core": {
"iri": "http://virtualflybrain.org/reports/Dolan2019",
"symbol": "",
"types": [
"Entity",
"Individual",
"DataSet"
],
"short_form": "Dolan2019",
"unique_facets": [],
"label": "GAL4 Split expression patterns from Dolan et al. 2019"
},
"link": ""
}
},
{
"license": {
"core": {
"iri": "http://virtualflybrain.org/reports/VFBlicense_CC_BY_4_0",
"symbol": "CC_BY",
"types": [
"Entity",
"Individual",
"License"
],
"short_form": "VFBlicense_CC_BY_4_0",
"unique_facets": [],
"label": "CC-BY_4.0"
},
"icon": "http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by.png",
"link": "https://creativecommons.org/licenses/by/4.0/legalcode"
},
"dataset": {
"core": {
"iri": "http://virtualflybrain.org/reports/FlyLight2019LateralHorn2019",
"symbol": "",
"types": [
"Entity",
"Individual",
"DataSet"
],
"short_form": "FlyLight2019LateralHorn2019",
"unique_facets": [],
"label": "FlyLight split-GAL4 lines for Lateral Horn"
},
"link": ""
}
},
{
"license": {
"core": {
"iri": "http://virtualflybrain.org/reports/VFBlicense_CC_BY_4_0",
"symbol": "CC_BY",
"types": [
"Entity",
"Individual",
"License"
],
"short_form": "VFBlicense_CC_BY_4_0",
"unique_facets": [],
"label": "CC-BY_4.0"
},
"icon": "http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by.png",
"link": "https://creativecommons.org/licenses/by/4.0/legalcode"
},
"dataset": {
"core": {
"iri": "http://virtualflybrain.org/reports/FafbDolan2019",
"symbol": "",
"types": [
"Entity",
"Individual",
"DataSet"
],
"short_form": "FafbDolan2019",
"unique_facets": [],
"label": "EM FAFB Dolan et al. 2019"
},
"link": "https://fafb.catmaid.virtualflybrain.org"
}
}
],
"pub_specific_content": {
"title": "Neurogenetic dissection of the Drosophila lateral horn reveals major outputs, diverse behavioural functions, and interactions with the mushroom body.",
"FlyBase": "FBrf0242477",
"PubMed": "31112130",
"DOI": "10.7554/eLife.43079"
}
}
63 changes: 63 additions & 0 deletions src/test/fixtures/term_info/pub_FBrf0243986.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"term": {
"core": {
"iri": "http://flybase.org/reports/FBrf0243986",
"symbol": "",
"types": [
"Entity",
"Individual",
"pub"
],
"short_form": "FBrf0243986",
"unique_facets": [
"pub"
],
"label": "Sayin et al., 2019, Neuron 104(3): 544--558.e6"
},
"description": [],
"comment": []
},
"query": "Get JSON for pub",
"version": "0dcd736",
"dataset_license": [
{
"license": {
"core": {
"iri": "http://virtualflybrain.org/reports/VFBlicense_CC_BY_4_0",
"symbol": "CC_BY",
"types": [
"Entity",
"Individual",
"License"
],
"short_form": "VFBlicense_CC_BY_4_0",
"unique_facets": [],
"label": "CC-BY_4.0"
},
"icon": "http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by.png",
"link": "https://creativecommons.org/licenses/by/4.0/legalcode"
},
"dataset": {
"core": {
"iri": "http://virtualflybrain.org/reports/Sayin2019",
"symbol": "",
"types": [
"Entity",
"Individual",
"DataSet"
],
"short_form": "Sayin2019",
"unique_facets": [],
"label": "EM FAFB Sayin et al 2019"
},
"link": "https://fafb.catmaid.virtualflybrain.org"
}
}
],
"pub_specific_content": {
"title": "A Neural Circuit Arbitrates between Persistence and Withdrawal in Hungry Drosophila.",
"FlyBase": "FBrf0243986",
"PubMed": "31471123",
"DOI": "10.1016/j.neuron.2019.07.028"
}
}
Loading
Loading