Skip to content

Commit 6ae5c88

Browse files
authored
Merge pull request #38 from VirtualFlyBrain/connectivity-testing
test connectivity queries and build term hierarchies
2 parents ac6b0c0 + 69ca81b commit 6ae5c88

11 files changed

Lines changed: 1279 additions & 192 deletions

.github/workflows/performance-test.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,11 @@ jobs:
225225
echo "" >> $GITHUB_STEP_SUMMARY
226226
cat performance.md >> $GITHUB_STEP_SUMMARY
227227
228-
- name: Commit Performance Report
229-
if: always()
228+
- name: Commit and Push Performance Report
229+
if: always() && github.ref == 'refs/heads/main'
230230
run: |
231231
git config --local user.email "action@github.com"
232232
git config --local user.name "GitHub Action"
233233
git add performance.md
234234
git diff --staged --quiet || git commit -m "Update performance test results [skip ci]"
235-
236-
- name: Push Performance Report
237-
if: always()
238-
uses: ad-m/github-push-action@master
239-
with:
240-
github_token: ${{ secrets.GITHUB_TOKEN }}
241-
branch: ${{ github.ref }}
235+
git push origin HEAD:main

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ test_results.py
1414
.vscode/settings.json
1515
temp_examples_output.txt
1616
json_block_*.json
17+
.idea/

src/test/term_info_queries_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def test_term_info_serialization_neuron_class2(self):
299299
self.assertFalse("thumbnail" in serialized)
300300

301301
self.assertTrue("references" in serialized)
302-
self.assertEqual(7, len(serialized["references"]))
302+
self.assertEqual(9, len(serialized["references"]))
303303

304304
self.assertTrue("targetingSplits" in serialized)
305305
self.assertEqual(6, len(serialized["targetingSplits"]))
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Tests for DownstreamClassConnectivity query.
2+
3+
Tests the query that finds downstream partner neuron classes for a given
4+
neuron class, using the pre-indexed downstream_connectivity_query Solr field.
5+
"""
6+
7+
import pytest
8+
import pandas as pd
9+
10+
from vfbquery.vfb_queries import (
11+
get_downstream_class_connectivity,
12+
DownstreamClassConnectivity_to_schema,
13+
)
14+
15+
# FBbt_00001482 = lineage NB3-2 primary interneuron — known to have
16+
# downstream_connectivity_query data in the vfb_json Solr core.
17+
TEST_CLASS = "FBbt_00001482"
18+
# A class that is unlikely to have downstream connectivity data.
19+
EMPTY_CLASS = "FBbt_00000001"
20+
21+
22+
class TestDownstreamClassConnectivityDict:
23+
"""Tests using return_dataframe=False (dict output)."""
24+
25+
@pytest.mark.integration
26+
def test_returns_results(self):
27+
result = get_downstream_class_connectivity(
28+
TEST_CLASS, return_dataframe=False, force_refresh=True
29+
)
30+
assert isinstance(result, dict)
31+
assert result["count"] > 0
32+
assert len(result["rows"]) > 0
33+
34+
@pytest.mark.integration
35+
def test_row_has_expected_keys(self):
36+
result = get_downstream_class_connectivity(
37+
TEST_CLASS, return_dataframe=False, limit=1, force_refresh=True
38+
)
39+
assert result["rows"], "Expected at least one row"
40+
row = result["rows"][0]
41+
expected_keys = {
42+
"id", "downstream_class", "total_n", "connected_n",
43+
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
44+
}
45+
assert expected_keys.issubset(row.keys())
46+
47+
@pytest.mark.integration
48+
def test_headers_present(self):
49+
result = get_downstream_class_connectivity(
50+
TEST_CLASS, return_dataframe=False, limit=1, force_refresh=True
51+
)
52+
assert "headers" in result
53+
assert "downstream_class" in result["headers"]
54+
55+
@pytest.mark.integration
56+
def test_limit_respected(self):
57+
result = get_downstream_class_connectivity(
58+
TEST_CLASS, return_dataframe=False, limit=3, force_refresh=True
59+
)
60+
assert len(result["rows"]) <= 3
61+
# count should reflect total, not the limited set
62+
assert result["count"] >= len(result["rows"])
63+
64+
@pytest.mark.integration
65+
def test_empty_class_returns_zero(self):
66+
result = get_downstream_class_connectivity(
67+
EMPTY_CLASS, return_dataframe=False, force_refresh=True
68+
)
69+
assert result["count"] == 0
70+
assert result["rows"] == []
71+
72+
73+
class TestDownstreamClassConnectivityDataFrame:
74+
"""Tests using return_dataframe=True (DataFrame output)."""
75+
76+
@pytest.mark.integration
77+
def test_returns_dataframe(self):
78+
df = get_downstream_class_connectivity(
79+
TEST_CLASS, return_dataframe=True, force_refresh=True
80+
)
81+
assert isinstance(df, pd.DataFrame)
82+
assert not df.empty
83+
84+
@pytest.mark.integration
85+
def test_dataframe_has_expected_columns(self):
86+
df = get_downstream_class_connectivity(
87+
TEST_CLASS, return_dataframe=True, limit=1, force_refresh=True
88+
)
89+
expected_cols = {
90+
"id", "downstream_class", "total_n", "connected_n",
91+
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
92+
}
93+
assert expected_cols.issubset(set(df.columns))
94+
95+
@pytest.mark.integration
96+
def test_limit_respected(self):
97+
df = get_downstream_class_connectivity(
98+
TEST_CLASS, return_dataframe=True, limit=5, force_refresh=True
99+
)
100+
assert len(df) <= 5
101+
102+
@pytest.mark.integration
103+
def test_empty_class_returns_empty_dataframe(self):
104+
df = get_downstream_class_connectivity(
105+
EMPTY_CLASS, return_dataframe=True, force_refresh=True
106+
)
107+
assert isinstance(df, pd.DataFrame)
108+
assert df.empty
109+
110+
111+
class TestDownstreamClassConnectivitySchema:
112+
def test_schema_generation(self):
113+
schema = DownstreamClassConnectivity_to_schema(
114+
"test neuron class", {"short_form": TEST_CLASS}
115+
)
116+
assert schema.query == "DownstreamClassConnectivity"
117+
assert schema.function == "get_downstream_class_connectivity"
118+
assert schema.preview == 5
119+
assert "downstream_class" in schema.preview_columns
120+
assert "percent_connected" in schema.preview_columns

src/test/test_hierarchy.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""Tests for get_hierarchy function.
2+
3+
Tests the hierarchy tree builder for both part_of (brain region structure)
4+
and subclass_of (cell type hierarchies), in both ancestor and descendant
5+
directions.
6+
"""
7+
8+
import pytest
9+
10+
from vfbquery.vfb_queries import get_hierarchy
11+
12+
13+
# Known test terms
14+
MUSHROOM_BODY = "FBbt_00005801"
15+
KENYON_CELL = "FBbt_00003686"
16+
17+
18+
class TestHierarchyValidation:
19+
def test_invalid_relationship_raises(self):
20+
with pytest.raises(ValueError, match="relationship"):
21+
get_hierarchy(KENYON_CELL, relationship="invalid")
22+
23+
def test_invalid_direction_raises(self):
24+
with pytest.raises(ValueError, match="direction"):
25+
get_hierarchy(KENYON_CELL, direction="invalid")
26+
27+
28+
class TestSubclassOfDescendants:
29+
@pytest.mark.integration
30+
def test_returns_descendants(self):
31+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
32+
assert result['id'] == KENYON_CELL
33+
assert result['label'] == 'Kenyon cell'
34+
assert result['relationship'] == 'subclass_of'
35+
assert 'descendants' in result
36+
assert len(result['descendants']) > 0
37+
38+
@pytest.mark.integration
39+
def test_descendants_have_id_and_label(self):
40+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
41+
for child in result['descendants']:
42+
assert 'id' in child
43+
assert 'label' in child
44+
assert child['id'].startswith('FBbt_')
45+
assert child['label'] != child['id'] # label should be resolved
46+
47+
@pytest.mark.integration
48+
def test_depth_1_has_no_grandchildren(self):
49+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
50+
for child in result['descendants']:
51+
assert 'descendants' not in child
52+
53+
@pytest.mark.integration
54+
def test_depth_2_has_nested_children(self):
55+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=2)
56+
has_grandchildren = any('descendants' in child for child in result['descendants'])
57+
assert has_grandchildren, "At least one direct subclass should have its own subclasses"
58+
59+
60+
class TestSubclassOfAncestors:
61+
@pytest.mark.integration
62+
def test_returns_ancestors(self):
63+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'ancestors', max_depth=1)
64+
assert 'ancestors' in result
65+
assert len(result['ancestors']) > 0
66+
67+
@pytest.mark.integration
68+
def test_ancestors_have_id_and_label(self):
69+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'ancestors', max_depth=1)
70+
for anc in result['ancestors']:
71+
assert 'id' in anc
72+
assert 'label' in anc
73+
74+
@pytest.mark.integration
75+
def test_kenyon_cell_ancestor_is_mb_intrinsic_neuron(self):
76+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'ancestors', max_depth=1)
77+
ancestor_ids = [a['id'] for a in result['ancestors']]
78+
assert 'FBbt_00007484' in ancestor_ids # mushroom body intrinsic neuron
79+
80+
@pytest.mark.integration
81+
def test_depth_2_has_nested_ancestors(self):
82+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'ancestors', max_depth=2)
83+
has_grandparent = any('ancestors' in anc for anc in result['ancestors'])
84+
assert has_grandparent
85+
86+
87+
class TestPartOfDescendants:
88+
@pytest.mark.integration
89+
def test_returns_parts(self):
90+
result = get_hierarchy(MUSHROOM_BODY, 'part_of', 'descendants', max_depth=1)
91+
assert result['id'] == MUSHROOM_BODY
92+
assert result['label'] == 'mushroom body'
93+
assert 'descendants' in result
94+
assert len(result['descendants']) > 0
95+
96+
@pytest.mark.integration
97+
def test_parts_have_id_and_label(self):
98+
result = get_hierarchy(MUSHROOM_BODY, 'part_of', 'descendants', max_depth=1)
99+
for part in result['descendants']:
100+
assert 'id' in part
101+
assert 'label' in part
102+
assert part['id'].startswith('FBbt_')
103+
104+
105+
class TestPartOfAncestors:
106+
@pytest.mark.integration
107+
def test_mushroom_body_part_of_protocerebrum(self):
108+
result = get_hierarchy(MUSHROOM_BODY, 'part_of', 'ancestors', max_depth=1)
109+
assert 'ancestors' in result
110+
ancestor_ids = [a['id'] for a in result['ancestors']]
111+
assert 'FBbt_00003627' in ancestor_ids # protocerebrum
112+
113+
114+
class TestDisplayOutput:
115+
@pytest.mark.integration
116+
def test_display_field_present(self):
117+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'both', max_depth=1)
118+
assert 'display' in result
119+
assert isinstance(result['display'], str)
120+
assert 'Kenyon cell' in result['display']
121+
122+
@pytest.mark.integration
123+
def test_display_shows_ancestors(self):
124+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'both', max_depth=1)
125+
assert 'ancestors' in result['display'].lower()
126+
assert 'mushroom body intrinsic neuron' in result['display']
127+
128+
@pytest.mark.integration
129+
def test_display_shows_tree_connectors(self):
130+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
131+
assert '├──' in result['display'] or '└──' in result['display']
132+
133+
@pytest.mark.integration
134+
def test_html_field_present(self):
135+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'both', max_depth=1)
136+
assert 'html' in result
137+
assert '<!DOCTYPE html>' in result['html']
138+
assert 'Kenyon cell' in result['html']
139+
140+
@pytest.mark.integration
141+
def test_html_contains_vfb_links(self):
142+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
143+
assert 'virtualflybrain.org' in result['html']
144+
assert KENYON_CELL in result['html']
145+
146+
147+
class TestBothDirections:
148+
@pytest.mark.integration
149+
def test_both_returns_ancestors_and_descendants(self):
150+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'both', max_depth=1)
151+
assert 'ancestors' in result
152+
assert 'descendants' in result
153+
assert len(result['ancestors']) > 0
154+
assert len(result['descendants']) > 0
155+
156+
@pytest.mark.integration
157+
def test_descendants_only_has_no_ancestors(self):
158+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'descendants', max_depth=1)
159+
assert 'descendants' in result
160+
assert 'ancestors' not in result
161+
162+
@pytest.mark.integration
163+
def test_ancestors_only_has_no_descendants(self):
164+
result = get_hierarchy(KENYON_CELL, 'subclass_of', 'ancestors', max_depth=1)
165+
assert 'ancestors' in result
166+
assert 'descendants' not in result

0 commit comments

Comments
 (0)