import json import re import sqlite3 import unittest from refresh_common_names import CURATED from taxonomy import COMMON_NAMES, DATABASE, DEFAULT_PATH, ICON_DIR, ICONS, MAX_ENGLISH, Taxonomy @unittest.skipUnless(DATABASE.exists(), "taxonomy snapshot unavailable") class TaxonomyTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.taxonomy = Taxonomy() def columns(self, html): return sorted({float(x) for x in re.findall(r'{scientific}', html) self.assertIn(f'class="tree-english">{english}', html) # A gloss that only repeats the scientific name is dropped by the build. self.assertIn('class="tree-label">Fungi', html) self.assertNotIn('class="tree-english">fungi', html) def test_one_tooltip_per_node(self): html = self.taxonomy.view(DEFAULT_PATH) # would render the browser's own tooltip on top of the styled one. self.assertNotIn("<title>", html) self.assertIn('data-detail="Viridiplantae (green plants) ยท', html) self.assertEqual(html.count("data-detail="), html.count('class="tree-hit"')) def test_curated_glosses_match_the_snapshot(self): with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn: for taxid, (scientific, english) in CURATED.items(): row = conn.execute("SELECT name FROM taxa WHERE taxid=?", (taxid,)).fetchone() self.assertIsNotNone(row, f"taxid {taxid} is not in the snapshot") self.assertEqual(row[0], scientific, f"taxid {taxid}") self.assertLessEqual(len(english), MAX_ENGLISH, f"taxid {taxid} gloss is clipped in the chart") def test_published_names_cover_the_curated_table(self): names = json.loads(COMMON_NAMES.read_text())["names"] redundant = {t for t, (scientific, english) in CURATED.items() if scientific.casefold() == english.casefold()} self.assertEqual({int(t) for t in names} & set(CURATED), set(CURATED) - redundant) self.assertEqual(names["6040"], "sponges") # straight from NCBI Taxonomy def test_icons_are_free_to_use_and_present(self): if not ICONS.exists(): self.skipTest("silhouettes unavailable") payload = json.loads(ICONS.read_text()) self.assertTrue(payload["images"]) for image, meta in payload["images"].items(): # NonCommercial is legally murky for a company-run Space and ShareAlike # would reach into the repository; refresh_icons.py filters both out. self.assertNotIn("-nc", meta["license"], image) self.assertNotIn("-sa", meta["license"], image) self.assertTrue((ICON_DIR / f"{image}.svg").is_file(), image) for taxid, image in payload["taxa"].items(): self.assertIn(image, payload["images"], taxid) def test_small_groups_inherit_an_ancestor_icon(self): if not self.taxonomy.icons: self.skipTest("silhouettes unavailable") with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn: conn.row_factory = sqlite3.Row own, borrowed = self.taxonomy.icon_for(conn, 2759) self.assertTrue(own) self.assertFalse(borrowed) # A taxon with no icon of its own still resolves, through its lineage. uncovered = next(r['taxid'] for r in conn.execute( "SELECT taxid FROM taxa WHERE total_count=1 ORDER BY taxid") if r['taxid'] in self.taxonomy.eukaryote_ids and r['taxid'] not in self.taxonomy.icons) image, borrowed = self.taxonomy.icon_for(conn, uncovered) self.assertTrue(image) self.assertTrue(borrowed) self.assertTrue((ICON_DIR / f"{image}.svg").is_file()) def test_borrowed_icons_never_repeat_within_a_column(self): if not self.taxonomy.icons: self.skipTest("silhouettes unavailable") for path in ("2759", DEFAULT_PATH, "2759/33154/33208", "2759/33154/33208/6072/33213"): html = self.taxonomy.view(path) # Columns are laid out left to right, so group the icons by their x. drawn = re.findall(r'<image class="tree-icon" x="([\d.]+)"[^>]*?/data/icons/([0-9a-f-]+)\.svg"', html) columns = {} for x, image in drawn: columns.setdefault(round(float(x)), []).append(image) for x, images in columns.items(): self.assertEqual(len(images), len(set(images)), f"{path} column at {x}") def test_chart_references_icons_by_url(self): if not self.taxonomy.icons: self.skipTest("silhouettes unavailable") html = self.taxonomy.view(DEFAULT_PATH) self.assertIn('class="tree-icon"', html) for image in re.findall(r'href="[^"]*/data/icons/([0-9a-f-]+)\.svg"', html): self.assertIn(image, self.taxonomy.icon_credits) def test_viewbox_spans_the_whole_chart(self): # A stray local named width or height inside the node loop once shrank the # viewBox to 23x23, which renders as a blank chart with every element intact. for path in ("2759", DEFAULT_PATH, "2759/33154/33208"): box = re.search(r'<svg viewBox="0 0 ([\d.]+) ([\d.]+)"', self.taxonomy.view(path)) self.assertIsNotNone(box, path) self.assertGreater(float(box.group(1)), 500, path) self.assertGreater(float(box.group(2)), 400, path) def test_rejects_paths_outside_the_tree(self): for path in ("1", "33208", "2759/33208", "2759/2759~3", "2759/9606", "abc"): with self.assertRaises(ValueError, msg=path): self.taxonomy.view(path) if __name__ == "__main__": unittest.main()