Skip to content

Commit 680ab0f

Browse files
authored
Merge pull request #51 from SystemsGenetics/functional-testing
Add functional tests for QR detector, metadata parsing, and tray summary
2 parents 50db60c + eddeadd commit 680ab0f

4 files changed

Lines changed: 257 additions & 1 deletion

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from Granny.Analyses.StarchArea import StarchArea
2+
from Granny.Models.Images.RGBImage import RGBImage
3+
4+
5+
def _get_analysis():
6+
"""Use StarchArea as a concrete implementation of Analysis."""
7+
return StarchArea()
8+
9+
10+
def test_parse_qr_from_filename_valid():
11+
analysis = _get_analysis()
12+
result = analysis._parse_qr_from_filename(
13+
"APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png"
14+
)
15+
assert result["project"] == "APPLE2025"
16+
assert result["lot"] == "LOT001"
17+
assert result["date"] == "2025-12-02"
18+
assert result["variety"] == "BB-Late"
19+
20+
21+
def test_parse_qr_from_filename_with_path():
22+
analysis = _get_analysis()
23+
result = analysis._parse_qr_from_filename(
24+
"/some/path/to/APPLE2025_LOT001_2025-12-02_BB-Late_fruit_05.png"
25+
)
26+
assert result["project"] == "APPLE2025"
27+
assert result["variety"] == "BB-Late"
28+
29+
30+
def test_parse_qr_from_filename_jpg():
31+
analysis = _get_analysis()
32+
result = analysis._parse_qr_from_filename(
33+
"PROJ_LOT_DATE_VAR_fruit_01.jpg"
34+
)
35+
assert result["project"] == "PROJ"
36+
assert result["variety"] == "VAR"
37+
38+
39+
def test_parse_qr_from_filename_legacy():
40+
"""Legacy filenames without QR data should return empty strings."""
41+
analysis = _get_analysis()
42+
result = analysis._parse_qr_from_filename("apple_fruit_01.png")
43+
assert result["project"] == ""
44+
assert result["lot"] == ""
45+
assert result["date"] == ""
46+
assert result["variety"] == ""
47+
48+
49+
def test_parse_qr_from_filename_no_match():
50+
analysis = _get_analysis()
51+
result = analysis._parse_qr_from_filename("random_image.png")
52+
assert result["project"] == ""
53+
54+
55+
def test_add_qr_metadata_valid():
56+
analysis = _get_analysis()
57+
img = RGBImage("APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png")
58+
analysis._add_qr_metadata(img, "APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png")
59+
60+
metadata = img.getMetaData()
61+
assert "project" in metadata
62+
assert metadata["project"].getValue() == "APPLE2025"
63+
assert metadata["lot"].getValue() == "LOT001"
64+
assert metadata["date"].getValue() == "2025-12-02"
65+
assert metadata["variety"].getValue() == "BB-Late"
66+
67+
68+
def test_add_qr_metadata_legacy():
69+
"""Legacy filenames should not add QR metadata."""
70+
analysis = _get_analysis()
71+
img = RGBImage("apple_fruit_01.png")
72+
analysis._add_qr_metadata(img, "apple_fruit_01.png")
73+
74+
metadata = img.getMetaData()
75+
assert "project" not in metadata
76+
assert "lot" not in metadata

tests/test_Models/test_Utils/__init__.py

Whitespace-only changes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import cv2
2+
import numpy as np
3+
from Granny.Utils.QRCodeDetector import QRCodeDetector
4+
5+
6+
def test_instantiation():
7+
detector = QRCodeDetector()
8+
assert detector.detector is not None
9+
assert isinstance(detector.barcode_enabled, bool)
10+
11+
12+
def test_detect_returns_none_for_blank_image():
13+
detector = QRCodeDetector()
14+
blank = np.zeros((100, 100, 3), dtype=np.uint8)
15+
data, points = detector.detect(blank)
16+
assert data is None
17+
assert points is None
18+
19+
20+
def test_detect_barcode_rotation_invariant():
21+
"""Barcode detection should work regardless of image rotation."""
22+
detector = QRCodeDetector()
23+
if not detector.barcode_enabled:
24+
return
25+
26+
# Create a test image with a Code128 barcode using pyzbar's expected input
27+
# We'll use a real barcode image if available, otherwise test the rotation logic
28+
# by generating a simple barcode-like pattern
29+
from pyzbar import pyzbar
30+
31+
# Create a synthetic barcode image using python-barcode if available
32+
try:
33+
import barcode
34+
from barcode.writer import ImageWriter
35+
import tempfile
36+
import os
37+
38+
code = barcode.get("code128", "TEST123", writer=ImageWriter())
39+
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
40+
code.save(tmp.name.replace(".png", ""))
41+
barcode_path = tmp.name.replace(".png", "") + ".png"
42+
43+
img = cv2.imread(barcode_path)
44+
if img is None:
45+
return
46+
47+
# Test original orientation
48+
data, points = detector._detect_barcode(img)
49+
assert data == "TEST123"
50+
51+
# Test 90 degree rotation
52+
rotated = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
53+
data, points = detector._detect_barcode(rotated)
54+
assert data == "TEST123"
55+
56+
# Test 180 degree rotation
57+
rotated = cv2.rotate(img, cv2.ROTATE_180)
58+
data, points = detector._detect_barcode(rotated)
59+
assert data == "TEST123"
60+
61+
# Test 270 degree rotation
62+
rotated = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
63+
data, points = detector._detect_barcode(rotated)
64+
assert data == "TEST123"
65+
66+
os.unlink(barcode_path)
67+
except ImportError:
68+
# python-barcode not installed, skip
69+
pass
70+
71+
72+
def test_extract_variety_info_pipe_format():
73+
detector = QRCodeDetector()
74+
info = detector.extract_variety_info("APPLE2026|LOT002|2026-01-23|BB-Early")
75+
assert info["project"] == "APPLE2026"
76+
assert info["lot"] == "LOT002"
77+
assert info["date"] == "2026-01-23"
78+
assert info["full"] == "BB-Early"
79+
assert info["variety"] == "BB"
80+
assert info["timing"] == "Early"
81+
82+
83+
def test_extract_variety_info_legacy_format():
84+
detector = QRCodeDetector()
85+
info = detector.extract_variety_info("BB-Late")
86+
assert info["project"] == "UNKNOWN"
87+
assert info["lot"] == "UNKNOWN"
88+
assert info["full"] == "BB-Late"
89+
assert info["variety"] == "BB"
90+
assert info["timing"] == "Late"
91+
92+
93+
def test_extract_variety_info_malformed_pipe():
94+
detector = QRCodeDetector()
95+
info = detector.extract_variety_info("only|two")
96+
assert info["project"] == "UNKNOWN"
97+
assert info["full"] == "only|two"
Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,88 @@
1+
import os
2+
import tempfile
3+
import pandas as pd
14
from Granny.Models.Values.MetaDataValue import MetaDataValue
5+
from Granny.Models.Values.FloatValue import FloatValue
6+
from Granny.Models.Values.StringValue import StringValue
7+
from Granny.Models.Images.RGBImage import RGBImage
8+
import numpy as np
29

310

11+
def _make_image(name, rating_val, project=None, lot=None, date=None, variety=None):
12+
"""Helper to create a test image with metadata."""
13+
img = RGBImage(name)
14+
img.setImage(np.zeros((10, 10, 3), dtype=np.uint8))
415

5-
16+
rating = FloatValue("rating", "rating", "test rating")
17+
rating.setMin(0.0)
18+
rating.setMax(1.0)
19+
rating.setValue(rating_val)
20+
img.addValue(rating)
21+
22+
if project:
23+
for key, val in [("project", project), ("lot", lot), ("date", date), ("variety", variety)]:
24+
sv = StringValue(key, key, f"test {key}")
25+
sv.setValue(val)
26+
img.addValue(sv)
27+
28+
return img
29+
30+
31+
def test_write_tray_summary_with_string_columns():
32+
"""tray_summary.csv should include string metadata columns like project, lot, date, variety."""
33+
with tempfile.TemporaryDirectory() as tmpdir:
34+
mdv = MetaDataValue("results", "results", "test")
35+
mdv.setValue(tmpdir)
36+
37+
images = [
38+
_make_image("PROJ_LOT1_2025-01-01_VAR_fruit_01.png", 0.8, "PROJ", "LOT1", "2025-01-01", "VAR"),
39+
_make_image("PROJ_LOT1_2025-01-01_VAR_fruit_02.png", 0.6, "PROJ", "LOT1", "2025-01-01", "VAR"),
40+
]
41+
mdv.setImageList(images)
42+
mdv.writeValue()
43+
44+
tray_df = pd.read_csv(os.path.join(tmpdir, "tray_summary.csv"))
45+
assert "project" in tray_df.columns
46+
assert "lot" in tray_df.columns
47+
assert "date" in tray_df.columns
48+
assert "variety" in tray_df.columns
49+
assert tray_df["project"].iloc[0] == "PROJ"
50+
assert tray_df["lot"].iloc[0] == "LOT1"
51+
assert tray_df["rating"].iloc[0] == 0.7 # average of 0.8 and 0.6
52+
53+
54+
def test_write_tray_summary_without_string_columns():
55+
"""tray_summary.csv should still work without string metadata."""
56+
with tempfile.TemporaryDirectory() as tmpdir:
57+
mdv = MetaDataValue("results", "results", "test")
58+
mdv.setValue(tmpdir)
59+
60+
images = [
61+
_make_image("apple_fruit_01.png", 0.9),
62+
_make_image("apple_fruit_02.png", 0.7),
63+
]
64+
mdv.setImageList(images)
65+
mdv.writeValue()
66+
67+
tray_df = pd.read_csv(os.path.join(tmpdir, "tray_summary.csv"))
68+
assert "TrayName" in tray_df.columns
69+
assert "rating" in tray_df.columns
70+
assert abs(tray_df["rating"].iloc[0] - 0.8) < 0.001
71+
72+
73+
def test_results_csv_has_all_rows():
74+
"""results.csv should have one row per image."""
75+
with tempfile.TemporaryDirectory() as tmpdir:
76+
mdv = MetaDataValue("results", "results", "test")
77+
mdv.setValue(tmpdir)
78+
79+
images = [
80+
_make_image("PROJ_LOT1_2025-01-01_VAR_fruit_01.png", 0.5, "PROJ", "LOT1", "2025-01-01", "VAR"),
81+
_make_image("PROJ_LOT1_2025-01-01_VAR_fruit_02.png", 0.6, "PROJ", "LOT1", "2025-01-01", "VAR"),
82+
_make_image("PROJ_LOT1_2025-01-01_VAR_fruit_03.png", 0.7, "PROJ", "LOT1", "2025-01-01", "VAR"),
83+
]
84+
mdv.setImageList(images)
85+
mdv.writeValue()
86+
87+
results_df = pd.read_csv(os.path.join(tmpdir, "results.csv"))
88+
assert len(results_df) == 3

0 commit comments

Comments
 (0)