- find_images: verify exact fruit_id prefix (stem-parse before _s0.) to prevent cross-fruit image contamination (e.g. mitschurins steals mitschurins_fruchtbare images) - DATA_DIR uses os.path.abspath to handle symlinks / relative invocation - counts["images"] derived from IMAGE_DIRS constant, not hardcoded keys - counts["warnings"] incremented on missing-id/name skip (was missing) - UPSERT preserves existing comment (comment = fruits.comment, not NULL) - Print summary and rollback moved inside try/except to avoid NameError - Remove dead XML_PATH constant - Tests: add tearDown to clean up tmpdir; fix vacuous _tn exclusion test to include positive-control _s0 file; add prefix-collision regression test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
6.9 KiB
Python
194 lines
6.9 KiB
Python
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from datetime import date
|
|
|
|
from import_fruits import map_typ, parse_synonyms, parse_date, find_images
|
|
|
|
|
|
class TestMapTyp(unittest.TestCase):
|
|
def test_maps_a_to_apfelsorten(self):
|
|
self.assertEqual(map_typ("a"), "Apfelsorten")
|
|
|
|
def test_maps_b_to_birnensorten(self):
|
|
self.assertEqual(map_typ("b"), "Birnensorten")
|
|
|
|
def test_maps_bq_aggregate_to_birnensorten(self):
|
|
self.assertEqual(map_typ("bq"), "Birnensorten")
|
|
|
|
def test_maps_q_to_quittensorten(self):
|
|
self.assertEqual(map_typ("q"), "Quittensorten")
|
|
|
|
def test_maps_apr_to_aprikosen(self):
|
|
self.assertEqual(map_typ("apr"), "Aprikosen")
|
|
|
|
def test_maps_aprpfi_aggregate_to_aprikosen(self):
|
|
self.assertEqual(map_typ("aprpfi"), "Aprikosen")
|
|
|
|
def test_maps_pfi_to_pfirsiche(self):
|
|
self.assertEqual(map_typ("pfi"), "Pfirsiche")
|
|
|
|
def test_maps_p_to_pfirsiche(self):
|
|
self.assertEqual(map_typ("p"), "Pfirsiche")
|
|
|
|
def test_maps_mir_to_mirabellen(self):
|
|
self.assertEqual(map_typ("mir"), "Mirabellen")
|
|
|
|
def test_maps_mirren_aggregate_to_mirabellen(self):
|
|
self.assertEqual(map_typ("mirren"), "Mirabellen")
|
|
|
|
def test_maps_ren_to_renekloden(self):
|
|
self.assertEqual(map_typ("ren"), "Renekloden")
|
|
|
|
def test_maps_pfl_to_pflaumen(self):
|
|
self.assertEqual(map_typ("pfl"), "Pflaumen")
|
|
|
|
def test_maps_pflzwe_aggregate_to_pflaumen(self):
|
|
self.assertEqual(map_typ("pflzwe"), "Pflaumen")
|
|
|
|
def test_maps_zwe_to_zwetschen(self):
|
|
self.assertEqual(map_typ("zwe"), "Zwetschen")
|
|
|
|
def test_maps_k_aggregate_to_sauerkirschen(self):
|
|
self.assertEqual(map_typ("k"), "Sauerkirschen")
|
|
|
|
def test_maps_bo_aggregate_to_brombeeren(self):
|
|
self.assertEqual(map_typ("bo"), "Brombeeren")
|
|
|
|
def test_maps_bob_to_brombeeren(self):
|
|
self.assertEqual(map_typ("bob"), "Brombeeren")
|
|
|
|
def test_maps_boe_to_erdbeeren(self):
|
|
self.assertEqual(map_typ("boe"), "Erdbeeren")
|
|
|
|
def test_maps_boh_to_himbeeren(self):
|
|
self.assertEqual(map_typ("boh"), "Himbeeren")
|
|
|
|
def test_maps_boj_to_johannisbeeren(self):
|
|
self.assertEqual(map_typ("boj"), "Johannisbeeren")
|
|
|
|
def test_maps_bos_to_stachelbeeren(self):
|
|
self.assertEqual(map_typ("bos"), "Stachelbeeren")
|
|
|
|
def test_maps_wei_to_wein(self):
|
|
self.assertEqual(map_typ("wei"), "Wein")
|
|
|
|
def test_maps_w_to_wein(self):
|
|
self.assertEqual(map_typ("w"), "Wein")
|
|
|
|
def test_maps_x_aggregate_to_apfelsorten(self):
|
|
self.assertEqual(map_typ("x"), "Apfelsorten")
|
|
|
|
def test_returns_none_for_unknown_typ(self):
|
|
self.assertIsNone(map_typ("zzz"))
|
|
|
|
|
|
class TestParseSynonyms(unittest.TestCase):
|
|
def test_splits_by_comma(self):
|
|
self.assertEqual(parse_synonyms("Boskop, Boskoop"), ["Boskop", "Boskoop"])
|
|
|
|
def test_strips_whitespace_and_newlines(self):
|
|
result = parse_synonyms("\nBoskop Renette [IH.1],\nBoskoop\n")
|
|
self.assertEqual(result, ["Boskop Renette [IH.1]", "Boskoop"])
|
|
|
|
def test_filters_empty_parts(self):
|
|
self.assertEqual(parse_synonyms(" , , Boskop ,"), ["Boskop"])
|
|
|
|
def test_empty_string_returns_empty_list(self):
|
|
self.assertEqual(parse_synonyms(""), [])
|
|
|
|
def test_single_synonym_no_comma(self):
|
|
self.assertEqual(parse_synonyms("White Paradise"), ["White Paradise"])
|
|
|
|
def test_none_returns_empty_list(self):
|
|
self.assertEqual(parse_synonyms(None), [])
|
|
|
|
|
|
class TestParseDate(unittest.TestCase):
|
|
def test_parses_yyyymmdd(self):
|
|
self.assertEqual(parse_date("20070806"), date(2007, 8, 6))
|
|
|
|
def test_empty_string_returns_none(self):
|
|
self.assertIsNone(parse_date(""))
|
|
|
|
def test_none_returns_none(self):
|
|
self.assertIsNone(parse_date(None))
|
|
|
|
def test_invalid_format_returns_none(self):
|
|
self.assertIsNone(parse_date("not-a-date"))
|
|
|
|
|
|
class TestFindImages(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.ef_dir = os.path.join(self.tmpdir, "einzelfruechte")
|
|
self.bl_dir = os.path.join(self.tmpdir, "blueten")
|
|
self.bau_dir = os.path.join(self.tmpdir, "baume")
|
|
for d in (self.ef_dir, self.bl_dir, self.bau_dir):
|
|
os.makedirs(d)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def _touch(self, path, content=b"img"):
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
|
|
def test_finds_fruit_image(self):
|
|
self._touch(os.path.join(self.ef_dir, "adams_apfel_ef_s0.jpg"))
|
|
imgs = find_images("adams_apfel", self.tmpdir)
|
|
types = [i["image_type"] for i in imgs]
|
|
self.assertIn("fruit", types)
|
|
|
|
def test_finds_flower_image(self):
|
|
self._touch(os.path.join(self.bl_dir, "adams_apfel_frucht_s0.jpg"))
|
|
imgs = find_images("adams_apfel", self.tmpdir)
|
|
types = [i["image_type"] for i in imgs]
|
|
self.assertIn("flower", types)
|
|
|
|
def test_finds_tree_image(self):
|
|
self._touch(os.path.join(self.bau_dir, "adams_apfel_bau_s0.jpg"))
|
|
imgs = find_images("adams_apfel", self.tmpdir)
|
|
types = [i["image_type"] for i in imgs]
|
|
self.assertIn("tree", types)
|
|
|
|
def test_skips_tn_files_but_finds_s0(self):
|
|
# _tn file must be excluded; _s0 file must be included (positive control)
|
|
self._touch(os.path.join(self.ef_dir, "adams_apfel_ef_tn.jpg"))
|
|
self._touch(os.path.join(self.ef_dir, "adams_apfel_ef_s0.jpg"))
|
|
imgs = find_images("adams_apfel", self.tmpdir)
|
|
self.assertEqual(len(imgs), 1)
|
|
self.assertEqual(imgs[0]["filename"], "adams_apfel_ef_s0.jpg")
|
|
|
|
def test_returns_empty_when_no_match(self):
|
|
imgs = find_images("unknown_fruit", self.tmpdir)
|
|
self.assertEqual(imgs, [])
|
|
|
|
def test_image_contains_bytes_and_filename(self):
|
|
content = b"\x89PNG\r\n"
|
|
self._touch(os.path.join(self.ef_dir, "adams_apfel_ef_s0.jpg"), content)
|
|
imgs = find_images("adams_apfel", self.tmpdir)
|
|
self.assertEqual(len(imgs), 1)
|
|
self.assertEqual(imgs[0]["data"], content)
|
|
self.assertEqual(imgs[0]["filename"], "adams_apfel_ef_s0.jpg")
|
|
|
|
def test_finds_all_three_types(self):
|
|
self._touch(os.path.join(self.ef_dir, "abbe_fetel_ef_s0.jpg"))
|
|
self._touch(os.path.join(self.bl_dir, "abbe_fetel_frucht_s0.jpg"))
|
|
self._touch(os.path.join(self.bau_dir, "abbe_fetel_bau_s0.jpg"))
|
|
imgs = find_images("abbe_fetel", self.tmpdir)
|
|
self.assertEqual(len(imgs), 3)
|
|
|
|
def test_does_not_steal_images_from_fruit_with_longer_id(self):
|
|
# "mitschurins" must not match "mitschurins_fruchtbare_nda_s0.jpg"
|
|
self._touch(os.path.join(self.ef_dir, "mitschurins_ef_s0.jpg"))
|
|
self._touch(os.path.join(self.ef_dir, "mitschurins_fruchtbare_ef_s0.jpg"))
|
|
imgs = find_images("mitschurins", self.tmpdir)
|
|
self.assertEqual(len(imgs), 1)
|
|
self.assertEqual(imgs[0]["filename"], "mitschurins_ef_s0.jpg")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|