Skip to content

Commit f992b46

Browse files
committed
prefer stocks matching full combination for splits
1 parent b1b497b commit f992b46

4 files changed

Lines changed: 363 additions & 24 deletions

File tree

‎src/test/test_expression_pattern_individual_queries.py‎

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,32 +106,74 @@ def test_instance_stock_query_matches_its_driver_live(self):
106106
"""Data-health (scheduled only): the same against live SOLR."""
107107
self.assertEqual(self._anchors_or_skip(self.EP_INDIVIDUAL), ["FBtp0060056"])
108108

109-
def test_split_class_offers_a_stock_query_per_hemidriver(self):
110-
"""Render code (PR-blocking): FindStocks is generated per hemidriver from a
111-
complete split fixture (its ``has_hemidriver`` relationships). Deterministic
112-
— isolates the generation code from live-data state. See TESTING.md
113-
'Fixture vs live (data_health)'."""
114-
self.assertEqual(
115-
_stock_anchors(_fixture_term_info("split_VFBexp_FBtp0129935FBtp0129968")),
116-
["FBtp0129935", "FBtp0129968"])
109+
SPLIT_COMBO = "FBco0001890" # FlyBase combination of the two
110+
111+
def _split_fixture_anchors(self, combos):
112+
"""Parse the split fixture with the FlyBase combination lookup stubbed,
113+
keeping the render test hermetic (no Chado)."""
114+
import vfbquery.flybase_stocks as fbs
115+
real = fbs.combinations_for_constructs
116+
fbs.combinations_for_constructs = combos
117+
try:
118+
return _stock_anchors(
119+
_fixture_term_info("split_VFBexp_FBtp0129935FBtp0129968"))
120+
finally:
121+
fbs.combinations_for_constructs = real
122+
123+
def test_split_class_offers_one_stock_query_on_its_combination(self):
124+
"""Render code (PR-blocking): a split's two ``has_hemidriver`` constructs
125+
collapse onto the FlyBase combination they build, so the menu offers a
126+
single FindStocks (exact combination first, hemidriver fallback) rather
127+
than one per half. See TESTING.md 'Fixture vs live (data_health)'."""
128+
seen = []
129+
130+
def combos(construct_ids):
131+
seen.append(sorted(construct_ids))
132+
return [self.SPLIT_COMBO]
133+
134+
self.assertEqual(self._split_fixture_anchors(combos), [self.SPLIT_COMBO])
135+
self.assertEqual(seen, [["FBtp0129935", "FBtp0129968"]])
136+
137+
def test_split_combination_stock_label_names_the_combination(self):
138+
"""Render code (PR-blocking): the label keeps its usual wording and names
139+
the FBco in brackets, where it used to name each FBtp."""
140+
import vfbquery.flybase_stocks as fbs
141+
real = fbs.combinations_for_constructs
142+
fbs.combinations_for_constructs = lambda ids: [self.SPLIT_COMBO]
143+
try:
144+
ti = _fixture_term_info("split_VFBexp_FBtp0129935FBtp0129968")
145+
finally:
146+
fbs.combinations_for_constructs = real
147+
labels = [q["label"] for q in ti["Queries"] if q.get("query") == "FindStocks"]
148+
self.assertEqual(labels, [f"Find fly stocks for {ti['Name']} ({self.SPLIT_COMBO})"])
149+
150+
def test_split_without_curated_combination_keeps_a_query_per_hemidriver(self):
151+
"""Render code (PR-blocking): no FlyBase combination -> per-hemidriver queries."""
152+
self.assertEqual(self._split_fixture_anchors(lambda ids: []),
153+
["FBtp0129935", "FBtp0129968"])
154+
155+
def test_split_combination_lookup_failure_keeps_a_query_per_hemidriver(self):
156+
"""Render code (PR-blocking): a Chado failure must not drop the stock queries."""
157+
def combos(ids):
158+
raise RuntimeError("chado down")
159+
self.assertEqual(self._split_fixture_anchors(combos),
160+
["FBtp0129935", "FBtp0129968"])
117161

118162
@pytest.mark.data_health
119-
def test_split_class_offers_a_stock_query_per_hemidriver_live(self):
163+
def test_split_class_offers_one_stock_query_on_its_combination_live(self):
120164
"""Data-health (scheduled only): the same against the live SOLR document, so an incomplete
121165
production split document (no ``Expression_pattern`` type / no
122166
``has_hemidriver``) is caught. Deselected on PRs via ``-m 'not data_health'``."""
123-
self.assertEqual(self._anchors_or_skip(self.SPLIT_CLASS),
124-
["FBtp0129935", "FBtp0129968"])
167+
self.assertEqual(self._anchors_or_skip(self.SPLIT_CLASS), [self.SPLIT_COMBO])
125168

126169
@pytest.mark.data_health
127-
def test_split_instance_inherits_hemidriver_stock_queries(self):
170+
def test_split_instance_inherits_combination_stock_query(self):
128171
# No own driver edge: the features come from the pattern class it
129172
# instantiates, which `_stock_features_via_parent_pattern` fetches with a
130173
# LIVE `_load_term_info(parent)` call. A fixture for the individual alone
131174
# cannot make this hermetic (the parent lookup still hits SOLR), so this
132175
# stays a live (data_health) test rather than a PR-blocking fixture one.
133-
self.assertEqual(self._anchors_or_skip(self.SPLIT_INDIVIDUAL),
134-
["FBtp0129935", "FBtp0129968"])
176+
self.assertEqual(self._anchors_or_skip(self.SPLIT_INDIVIDUAL), [self.SPLIT_COMBO])
135177

136178

137179
class TestExpressionPatternIndividualQueries(unittest.TestCase):

‎src/test/test_flybase_stocks.py‎

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""Tests for flybase_stocks module — entity resolution and stock discovery."""
2+
import pandas as pd
23
import pytest
34

4-
from vfbquery.flybase_stocks import resolve_entity, find_stocks
5+
from vfbquery.flybase_stocks import (
6+
COMBO_MATCH_EXACT,
7+
COMBO_MATCH_HEMIDRIVER,
8+
COMBO_MATCH_OTHER_COMBO,
9+
resolve_entity,
10+
find_stocks,
11+
)
512
from vfbquery.vfb_queries import (
613
_flybase_report_url,
714
_md_link,
@@ -231,6 +238,101 @@ def test_nonexistent_combination(self):
231238
stocks = find_stocks("FBco9999999")
232239
assert stocks == []
233240

241+
@pytest.mark.integration
242+
def test_exact_combination_hides_hemidriver_stocks(self):
243+
# FBco0001000 = R50C12-p65.AD ∩ VT049369-GAL4.DBD; 607102 carries both,
244+
# so the single-hemidriver stocks (89605, 72840, 603713) are dropped.
245+
stocks = find_stocks("FBco0001000")
246+
assert [s["stock_number"] for s in stocks] == ["607102"]
247+
assert stocks[0]["match"] == COMBO_MATCH_EXACT
248+
249+
@pytest.mark.integration
250+
def test_fallback_is_per_hemidriver(self):
251+
# FBco0002000 has no exact stock. Its R20A03-GAL4.DBD has stocks of its
252+
# own, so its other-combination stocks are hidden; its R82F03-p65.AD has
253+
# none, so it falls back to the stocks pairing it with other DBDs.
254+
stocks = {s["stock_number"]: s["match"] for s in find_stocks("FBco0002000")}
255+
assert stocks["75808"] == COMBO_MATCH_HEMIDRIVER # R20A03 DBD alone
256+
assert stocks["602487"] == COMBO_MATCH_OTHER_COMBO # R82F03 AD + R48G01 DBD
257+
assert "602978" not in stocks # R20A03 DBD + R52B07 AD
258+
259+
@pytest.mark.integration
260+
def test_uncurated_partner_hemidriver_is_detected(self):
261+
# 88099 pairs R24H08-GAL4.DBD with VT058427-p65.AD, which is in no FBco,
262+
# so it must be recognised by the split-system tool it encodes.
263+
import vfbquery.flybase_stocks as fbs
264+
from vfbquery.flybase_db import get_connection
265+
conn = get_connection(statement_timeout_ms=60000)
266+
try:
267+
df = fbs._run_query(conn, fbs._STOCK_HEMIDRIVERS_SQL,
268+
{"stock_ids": ["FBst0088099"]})
269+
finally:
270+
conn.close()
271+
assert "FBal0331043" in set(df["allele_id"])
272+
273+
@pytest.mark.integration
274+
def test_match_column_only_for_combinations(self):
275+
combo = get_flybase_stocks("FBco0001000", return_dataframe=False)
276+
assert combo["headers"]["match"]["type"] == "text"
277+
assert combo["rows"][0]["match"] == COMBO_MATCH_EXACT
278+
allele = get_flybase_stocks("FBal0034227", return_dataframe=False, limit=3)
279+
assert "match" not in allele["headers"]
280+
281+
282+
class TestCombinationRanking:
283+
"""Ranking of combination stocks, with the chado queries stubbed out."""
284+
285+
AD, DBD, OTHER_DBD = "FBal_AD", "FBal_DBD", "FBal_DBD2"
286+
287+
def _stocks(self, monkeypatch, per_allele, hemidrivers):
288+
import vfbquery.flybase_stocks as fbs
289+
290+
def fake_run_query(conn, sql, params):
291+
if sql is fbs._COMBO_COMPONENTS_SQL:
292+
return pd.DataFrame({"allele_name": ["AD", "DBD"],
293+
"allele_id": [self.AD, self.DBD]})
294+
if sql is fbs._STOCK_HEMIDRIVERS_SQL:
295+
return pd.DataFrame(hemidrivers, columns=["stock_id", "allele_id"])
296+
raise AssertionError("unexpected query")
297+
298+
def fake_allele(conn, allele_id, collection_filter=None):
299+
return pd.DataFrame(
300+
[{"stock_id": sid, "stock_number": sid[-1], "genotype": "",
301+
"collection": "BDSC"} for sid in per_allele.get(allele_id, [])],
302+
columns=["stock_id", "stock_number", "genotype", "collection"])
303+
304+
monkeypatch.setattr(fbs, "_run_query", fake_run_query)
305+
monkeypatch.setattr(fbs, "_find_stocks_allele", fake_allele)
306+
df = fbs._find_stocks_combination(None, "FBco_test")
307+
return list(zip(df["stock_id"], df["match"]))
308+
309+
def test_exact_combination_hides_lower_tiers(self, monkeypatch):
310+
got = self._stocks(
311+
monkeypatch,
312+
per_allele={self.AD: ["st1", "st2", "st3"], self.DBD: ["st3", "st4"]},
313+
hemidrivers=[("st1", self.AD), ("st1", self.OTHER_DBD), ("st2", self.AD),
314+
("st3", self.AD), ("st3", self.DBD), ("st4", self.DBD)])
315+
assert got == [("st3", COMBO_MATCH_EXACT)]
316+
317+
def test_hemidriver_alone_hides_its_other_combinations(self, monkeypatch):
318+
# AD has a stock of its own (st2), so st1 is hidden; DBD has only st5.
319+
got = self._stocks(
320+
monkeypatch,
321+
per_allele={self.AD: ["st1", "st2"], self.DBD: ["st4", "st5"]},
322+
hemidrivers=[("st1", self.AD), ("st1", self.OTHER_DBD), ("st2", self.AD),
323+
("st4", self.DBD), ("st5", self.DBD)])
324+
assert got == [("st2", COMBO_MATCH_HEMIDRIVER),
325+
("st4", COMBO_MATCH_HEMIDRIVER),
326+
("st5", COMBO_MATCH_HEMIDRIVER)]
327+
328+
def test_hemidriver_without_own_stock_falls_back_to_other_combinations(self, monkeypatch):
329+
# DBD has a stock of its own; AD only appears paired with another DBD.
330+
got = self._stocks(
331+
monkeypatch,
332+
per_allele={self.AD: ["st1"], self.DBD: ["st2"]},
333+
hemidrivers=[("st1", self.AD), ("st1", self.OTHER_DBD), ("st2", self.DBD)])
334+
assert got == [("st2", COMBO_MATCH_HEMIDRIVER), ("st1", COMBO_MATCH_OTHER_COMBO)]
335+
234336

235337
class TestFindStocksTableSchema:
236338
CONSTRUCT_WITH_STOCKS = "FBtp0000352" # P{GawB}

0 commit comments

Comments
 (0)