|
1 | 1 | """ |
2 | | -Test suite for ExpressionOverlapsHere query (get_expression_overlaps_here). |
| 2 | +Test suite for AnatomyExpressedIn query (get_expression_overlaps_here). |
3 | 3 |
|
4 | | -INVERSE-direction query as of VFBquery v1.13.6 — given an expression |
5 | | -pattern, return the anatomy classes whose Individuals overlap with the |
6 | | -pattern's Individuals. The forward direction (anatomy -> expression |
7 | | -patterns) is now solely owned by TransgeneExpressionHere. |
| 4 | +INVERSE-direction query — given an expression pattern, return the anatomy |
| 5 | +classes whose Individuals overlap with the pattern's Individuals. The |
| 6 | +forward direction (anatomy -> expression patterns) is solely owned by |
| 7 | +TransgeneExpressionHere. |
8 | 8 |
|
9 | 9 | XMI Source: https://raw.githubusercontent.com/VirtualFlyBrain/geppetto-vfb/master/model/vfb.xmi |
10 | | -Query: ExpressionOverlapsHere ("Anatomy $NAME is expressed in") |
| 10 | +Query: AnatomyExpressedIn ("Anatomy where $NAME is expressed") |
11 | 11 | """ |
12 | 12 |
|
13 | 13 | import unittest |
|
20 | 20 | from vfbquery import vfb_queries as vq |
21 | 21 |
|
22 | 22 |
|
23 | | -class TestExpressionOverlapsHere(unittest.TestCase): |
| 23 | +class TestAnatomyExpressedIn(unittest.TestCase): |
24 | 24 | """Test cases for get_expression_overlaps_here function""" |
25 | 25 |
|
26 | | - def test_expression_overlaps_basic_dataframe(self): |
| 26 | + def test_anatomy_expressed_in_basic_dataframe(self): |
27 | 27 | """Test basic query returns DataFrame with expected columns""" |
28 | | - # Test with adult brain (FBbt_00003982) - known to have expression patterns |
29 | 28 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=True) |
30 | | - |
| 29 | + |
31 | 30 | self.assertIsInstance(result, pd.DataFrame, "Should return pandas DataFrame") |
32 | | - |
| 31 | + |
33 | 32 | if not result.empty: |
34 | | - # Check for expected columns |
35 | 33 | expected_columns = ['id', 'name', 'tags', 'pubs'] |
36 | 34 | for col in expected_columns: |
37 | 35 | self.assertIn(col, result.columns, f"DataFrame should contain '{col}' column") |
38 | | - |
39 | | - # Verify data types |
| 36 | + |
40 | 37 | self.assertTrue(all(isinstance(x, str) for x in result['id']), "IDs should be strings") |
41 | 38 | self.assertTrue(all(isinstance(x, str) for x in result['name']), "Names should be strings") |
42 | | - |
43 | | - print(f"\n✓ Found {len(result)} expression patterns overlapping FBbt_00003982") |
44 | | - print(f"✓ Sample results: {result.head(3)[['id', 'name']].to_dict('records')}") |
45 | 39 |
|
46 | | - def test_expression_overlaps_formatted_output(self): |
| 40 | + print(f"\nFound {len(result)} anatomy classes where VFBexp_FBtp0001321 is expressed") |
| 41 | + print(f"Sample results: {result.head(3)[['id', 'name']].to_dict('records')}") |
| 42 | + |
| 43 | + def test_anatomy_expressed_in_formatted_output(self): |
47 | 44 | """Test query returns properly formatted dictionary output""" |
48 | 45 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=False) |
49 | | - |
| 46 | + |
50 | 47 | self.assertIsInstance(result, dict, "Should return dictionary when return_dataframe=False") |
51 | | - |
52 | | - # Check structure |
| 48 | + |
53 | 49 | self.assertIn('headers', result, "Result should contain 'headers'") |
54 | 50 | self.assertIn('rows', result, "Result should contain 'rows'") |
55 | 51 | self.assertIn('count', result, "Result should contain 'count'") |
56 | | - |
57 | | - # Check headers structure |
| 52 | + |
58 | 53 | headers = result['headers'] |
59 | 54 | expected_headers = ['id', 'name', 'tags', 'pubs'] |
60 | 55 | for header in expected_headers: |
61 | 56 | self.assertIn(header, headers, f"Headers should contain '{header}'") |
62 | 57 | self.assertIn('title', headers[header], f"Header '{header}' should have 'title'") |
63 | 58 | self.assertIn('type', headers[header], f"Header '{header}' should have 'type'") |
64 | 59 | self.assertIn('order', headers[header], f"Header '{header}' should have 'order'") |
65 | | - |
66 | | - # Verify header types |
| 60 | + |
67 | 61 | self.assertEqual(headers['id']['type'], 'selection_id') |
68 | 62 | self.assertEqual(headers['name']['type'], 'markdown') |
69 | 63 | self.assertEqual(headers['tags']['type'], 'tags') |
70 | 64 | self.assertEqual(headers['pubs']['type'], 'metadata') |
71 | | - |
| 65 | + |
72 | 66 | if result['rows']: |
73 | | - # Check row structure |
74 | 67 | first_row = result['rows'][0] |
75 | 68 | for key in expected_headers: |
76 | 69 | self.assertIn(key, first_row, f"Row should contain '{key}'") |
77 | | - |
78 | | - print(f"\n✓ Formatted output contains {result['count']} expression patterns") |
79 | | - print(f"✓ Sample row keys: {list(first_row.keys())}") |
80 | 70 |
|
81 | | - def test_expression_overlaps_limit(self): |
| 71 | + print(f"\nFormatted output contains {result['count']} anatomy classes") |
| 72 | + print(f"Sample row keys: {list(first_row.keys())}") |
| 73 | + |
| 74 | + def test_anatomy_expressed_in_limit(self): |
82 | 75 | """Test limit parameter restricts number of results""" |
83 | 76 | limit = 3 |
84 | 77 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=True, limit=limit) |
85 | | - |
| 78 | + |
86 | 79 | if not result.empty: |
87 | 80 | self.assertLessEqual(len(result), limit, f"Should return at most {limit} results") |
88 | | - print(f"\n✓ Limit parameter working: requested {limit}, got {len(result)}") |
| 81 | + print(f"\nLimit parameter working: requested {limit}, got {len(result)}") |
| 82 | + |
| 83 | + def test_anatomy_expressed_in_empty_result(self): |
| 84 | + """Test query with an id that has no expression overlaps""" |
| 85 | + result = vq.get_expression_overlaps_here('VFBexp_99999999', return_dataframe=True) |
89 | 86 |
|
90 | | - def test_expression_overlaps_empty_result(self): |
91 | | - """Test query with anatomy that has no expression patterns""" |
92 | | - # Use a very specific anatomy term unlikely to have expression patterns |
93 | | - result = vq.get_expression_overlaps_here('FBbt_99999999', return_dataframe=True) |
94 | | - |
95 | 87 | # Should return empty DataFrame, not error |
96 | 88 | self.assertIsInstance(result, pd.DataFrame, "Should return DataFrame even for no results") |
97 | | - print(f"\n✓ Empty result handling works correctly") |
| 89 | + print(f"\nEmpty result handling works correctly") |
98 | 90 |
|
99 | | - def test_expression_overlaps_publication_data(self): |
| 91 | + def test_anatomy_expressed_in_publication_data(self): |
100 | 92 | """Test that publication data is properly formatted when present""" |
101 | 93 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=True, limit=10) |
102 | | - |
| 94 | + |
103 | 95 | if not result.empty: |
104 | | - # Check if pubs column exists and contains data |
105 | 96 | self.assertIn('pubs', result.columns, "Should have 'pubs' column") |
106 | | - |
107 | | - # Check structure of publication data |
| 97 | + |
108 | 98 | for idx, row in result.iterrows(): |
109 | | - if row['pubs']: # If publications exist |
| 99 | + if row['pubs']: |
110 | 100 | pubs = row['pubs'] |
111 | 101 | self.assertIsInstance(pubs, list, "Publications should be a list") |
112 | | - |
113 | | - if pubs: # If list is not empty |
| 102 | + |
| 103 | + if pubs: |
114 | 104 | first_pub = pubs[0] |
115 | 105 | self.assertIsInstance(first_pub, dict, "Publication should be a dict") |
116 | | - |
117 | | - # Check expected publication fields |
| 106 | + |
118 | 107 | if 'core' in first_pub: |
119 | 108 | self.assertIn('short_form', first_pub['core'], "Publication should have short_form") |
120 | | - |
121 | | - print(f"\n✓ Publication data properly structured") |
| 109 | + |
| 110 | + print(f"\nPublication data properly structured") |
122 | 111 | break |
123 | 112 |
|
124 | | - def test_expression_overlaps_markdown_encoding(self): |
| 113 | + def test_anatomy_expressed_in_markdown_encoding(self): |
125 | 114 | """Test that markdown links are properly formatted""" |
126 | 115 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=True, limit=5) |
127 | | - |
| 116 | + |
128 | 117 | if not result.empty: |
129 | | - # Check that names contain markdown link format [label](url) |
130 | 118 | for name in result['name']: |
131 | | - # Should have markdown link format |
132 | 119 | self.assertIn('[', name, "Name should contain markdown link start") |
133 | 120 | self.assertIn('](', name, "Name should contain markdown link separator") |
134 | 121 | self.assertIn(')', name, "Name should contain markdown link end") |
135 | | - |
136 | | - print(f"\n✓ Markdown links properly formatted") |
137 | 122 |
|
138 | | - def test_expression_overlaps_tags_format(self): |
| 123 | + print(f"\nMarkdown links properly formatted") |
| 124 | + |
| 125 | + def test_anatomy_expressed_in_tags_format(self): |
139 | 126 | """Test that tags are properly formatted as pipe-separated strings""" |
140 | 127 | result = vq.get_expression_overlaps_here('VFBexp_FBtp0001321', return_dataframe=True, limit=5) |
141 | | - |
| 128 | + |
142 | 129 | if not result.empty and 'tags' in result.columns: |
143 | 130 | for tags in result['tags']: |
144 | 131 | if pd.notna(tags) and tags: |
145 | | - # Tags should be pipe-separated strings |
146 | 132 | self.assertIsInstance(tags, str, "Tags should be string type") |
147 | | - # Could contain pipes for multiple tags |
148 | 133 | parts = tags.split('|') |
149 | 134 | self.assertTrue(all(isinstance(p, str) for p in parts), "Tag parts should be strings") |
150 | | - |
151 | | - print(f"\n✓ Tags format verified") |
| 135 | + |
| 136 | + print(f"\nTags format verified") |
152 | 137 |
|
153 | 138 |
|
154 | 139 | class TestAnatomyExpressedInSchema(unittest.TestCase): |
155 | | - """Test cases for AnatomyExpressedIn_to_schema (renamed from |
156 | | - ExpressionOverlapsHere_to_schema in v1.13.7). The legacy name is |
157 | | - kept as a back-compat alias and must continue to import. |
158 | | - """ |
| 140 | + """Test cases for AnatomyExpressedIn_to_schema.""" |
159 | 141 |
|
160 | 142 | def test_schema_function_exists(self): |
161 | | - """Test that both the canonical and legacy schema functions are defined.""" |
| 143 | + """Canonical schema function is defined; legacy alias is gone.""" |
162 | 144 | self.assertTrue(hasattr(vq, 'AnatomyExpressedIn_to_schema'), |
163 | | - "AnatomyExpressedIn_to_schema function should exist") |
164 | | - self.assertTrue(hasattr(vq, 'ExpressionOverlapsHere_to_schema'), |
165 | | - "Legacy ExpressionOverlapsHere_to_schema alias should still exist") |
| 145 | + "AnatomyExpressedIn_to_schema function should exist") |
| 146 | + self.assertFalse(hasattr(vq, 'ExpressionOverlapsHere_to_schema'), |
| 147 | + "Legacy ExpressionOverlapsHere_to_schema alias must be removed") |
166 | 148 |
|
167 | 149 | def test_schema_structure(self): |
168 | | - """Test that schema function returns proper Query object.""" |
| 150 | + """Schema function returns the expected Query object.""" |
169 | 151 | from vfbquery.vfb_queries import AnatomyExpressedIn_to_schema |
170 | 152 |
|
171 | 153 | schema = AnatomyExpressedIn_to_schema( |
172 | 154 | "P{GAL4-per.BS} expression pattern", |
173 | 155 | {"short_form": "VFBexp_FBtp0001321"}, |
174 | 156 | ) |
175 | 157 |
|
176 | | - # Check Query object attributes |
177 | 158 | self.assertEqual(schema.query, "AnatomyExpressedIn") |
178 | 159 | self.assertEqual(schema.function, "get_expression_overlaps_here") |
179 | 160 | self.assertIn("Anatomy where", schema.label) |
180 | 161 | self.assertEqual(schema.preview, 5) |
181 | 162 | self.assertEqual(schema.preview_columns, ["id", "name", "tags", "pubs"]) |
182 | | - |
183 | | - # Check takes structure |
| 163 | + |
| 164 | + # takes constrains the input to an expression pattern (or fragment) |
184 | 165 | self.assertIn("short_form", schema.takes) |
185 | 166 | self.assertIn("default", schema.takes) |
186 | | - self.assertEqual(schema.takes["short_form"], {"$and": ["Class", "Anatomy"]}) |
187 | | - |
188 | | - print("\n✓ Schema structure verified") |
| 167 | + self.assertEqual( |
| 168 | + schema.takes["short_form"], |
| 169 | + {"$or": [ |
| 170 | + {"$and": ["Class", "Expression_pattern"]}, |
| 171 | + {"$and": ["Class", "Expression_pattern_fragment"]}, |
| 172 | + ]}, |
| 173 | + ) |
| 174 | + |
| 175 | + print("\nSchema structure verified") |
| 176 | + |
| 177 | + |
| 178 | +class TestAnatomyExpressedInWireMapping(unittest.TestCase): |
| 179 | + """ha_api.QUERY_TYPE_MAP — canonical key only, legacy alias removed.""" |
| 180 | + |
| 181 | + def test_query_type_map(self): |
| 182 | + from vfbquery.ha_api import QUERY_TYPE_MAP |
| 183 | + self.assertIn("AnatomyExpressedIn", QUERY_TYPE_MAP, |
| 184 | + "AnatomyExpressedIn must be a recognised query_type") |
| 185 | + self.assertEqual(QUERY_TYPE_MAP["AnatomyExpressedIn"], |
| 186 | + "get_expression_overlaps_here") |
| 187 | + self.assertNotIn("ExpressionOverlapsHere", QUERY_TYPE_MAP, |
| 188 | + "Legacy ExpressionOverlapsHere alias must be removed") |
189 | 189 |
|
190 | 190 |
|
191 | 191 | if __name__ == '__main__': |
192 | | - # Run tests with verbose output |
193 | 192 | unittest.main(verbosity=2) |
0 commit comments