carbon-a-database-explorer / tests /test_taxonomy.py
lvwerra's picture
lvwerra HF Staff
Add clade silhouettes, compact navigation tabs, and drop the hover underline
aa5d49c verified
Raw History Blame
7.08 kB
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'<rect x="([\d.]+)" y="[\d.]+" width="14"', html)})
def test_expansion_keeps_every_ancestor_column(self):
self.assertEqual(len(self.columns(self.taxonomy.view("2759"))), 2)
self.assertEqual(len(self.columns(self.taxonomy.view(DEFAULT_PATH))), 3)
html = self.taxonomy.view("2759/33154/33208")
self.assertEqual(len(self.columns(html)), 4)
self.assertEqual(html.count('class="tree-node is-selected"'), 3)
def test_search_path_expands_lineage(self):
path = self.taxonomy.taxon_path(9606)
self.assertTrue(path.startswith("2759/33154/33208/"))
self.assertTrue(path.endswith("/9606"))
self.assertIn(f'data-route="{path}"', self.taxonomy.view_taxon(9606))
def test_other_lineages_expand_as_columns(self):
html = self.taxonomy.view("2759/2759~6")
self.assertIn('data-path="2759/2759~6/2759~12"', html)
def test_english_names_label_latin_clades(self):
html = self.taxonomy.view("2759/33154/33208")
for scientific, english in (("Opisthokonta", "animals and fungi"), ("Metazoa", "animals"),
("Eumetazoa", "animals with true tissues"), ("Porifera", "sponges")):
self.assertIn(f'class="tree-label">{scientific}</text>', html)
self.assertIn(f'class="tree-english">{english}</text>', html)
# A gloss that only repeats the scientific name is dropped by the build.
self.assertIn('class="tree-label">Fungi</text>', html)
self.assertNotIn('class="tree-english">fungi</text>', html)
def test_one_tooltip_per_node(self):
html = self.taxonomy.view(DEFAULT_PATH)
# <title> 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()