diff --git a/src/test/test_template_no_painted_domains.py b/src/test/test_template_no_painted_domains.py new file mode 100644 index 0000000..74da553 --- /dev/null +++ b/src/test/test_template_no_painted_domains.py @@ -0,0 +1,61 @@ +""" +Regression test for get_term_info on templates that have NO painted anatomical +domains. + +VFB_00050000 (L1 larval CNS ssTEM - Cardona/Janelia, the "Seymour" L1EM larval +template) carries a single *empty* template_domain from the pipeline +(index: [], null anatomical_individual). The term_info parser used to do +``int(image.index[0])`` on every domain unconditionally, so the empty index +list raised ``IndexError: list index out of range``. That exception propagated +to get_term_info's ``except IndexError`` handler, which mis-reported it as a +SOLR access failure ("Error accessing SOLR server!") and returned ``None`` — +i.e. the endpoint served ``null`` for a perfectly valid, indexed template. + +This test asserts the template resolves to a real term-info object with the +degenerate domain skipped rather than crashing the whole lookup. +""" + +import os +import unittest +import sys + +# Add src directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from vfbquery.vfb_queries import get_term_info + + +class TemplateNoPaintedDomainsTest(unittest.TestCase): + """A template with no painted domains must still resolve, not return null.""" + + NO_DOMAIN_TEMPLATE = 'VFB_00050000' # L1EM larval template ("Seymour") + + def setUp(self): + # Force the live, synchronous parse path (bypass the two-phase preview + # warm and the shared production cache) so the fix is actually exercised. + self._prev_cache = os.environ.get('VFBQUERY_CACHE_ENABLED') + os.environ['VFBQUERY_CACHE_ENABLED'] = 'false' + + def tearDown(self): + if self._prev_cache is None: + os.environ.pop('VFBQUERY_CACHE_ENABLED', None) + else: + os.environ['VFBQUERY_CACHE_ENABLED'] = self._prev_cache + + def test_template_without_painted_domains_resolves(self): + result = get_term_info(self.NO_DOMAIN_TEMPLATE) + # The regression: this used to be None (-> null over HTTP). + self.assertIsNotNone( + result, + f"get_term_info('{self.NO_DOMAIN_TEMPLATE}') returned None; the empty " + f"template_domain must be skipped, not crash the whole lookup." + ) + self.assertEqual(result.get('Id'), self.NO_DOMAIN_TEMPLATE) + self.assertTrue(result.get('IsTemplate'), "Template flag should be set") + # An empty/degenerate domain must be dropped, never emitted with a bogus index. + for idx in (result.get('Domains') or {}): + self.assertIsNotNone(idx) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 16321fe..38ede4b 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -997,6 +997,16 @@ def term_info_parse_object(results, short_form): images = {} termInfo["IsTemplate"] = True for image in vfbTerm.template_domains: + # Skip degenerate/placeholder domains. Templates that carry no + # painted anatomical domains (e.g. VFB_00050000, the L1EM larval + # template) still emit a single empty domain from the pipeline + # (index: [], null anatomical_individual). Without this guard the + # int(image.index[0]) below raised IndexError, which propagated to + # get_term_info's `except IndexError` and was mis-reported as a SOLR + # access failure, returning null for the whole term. Mirrors the + # `if len(image.index) > 0` guard already used for template_channel. + if not image.index or len(image.index) == 0: + continue record = {} record["id"] = image.anatomical_individual.short_form label = image.anatomical_individual.label