|
1 | 1 | """Tests for flybase_stocks module — entity resolution and stock discovery.""" |
| 2 | +import pandas as pd |
2 | 3 | import pytest |
3 | 4 |
|
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 | +) |
5 | 12 | from vfbquery.vfb_queries import ( |
6 | 13 | _flybase_report_url, |
7 | 14 | _md_link, |
@@ -231,6 +238,101 @@ def test_nonexistent_combination(self): |
231 | 238 | stocks = find_stocks("FBco9999999") |
232 | 239 | assert stocks == [] |
233 | 240 |
|
| 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 | + |
234 | 336 |
|
235 | 337 | class TestFindStocksTableSchema: |
236 | 338 | CONSTRUCT_WITH_STOCKS = "FBtp0000352" # P{GawB} |
|
0 commit comments