diff --git a/tests/test_Analyses/test_Analysis.py b/tests/test_Analyses/test_Analysis.py new file mode 100644 index 0000000..ce0cd59 --- /dev/null +++ b/tests/test_Analyses/test_Analysis.py @@ -0,0 +1,76 @@ +from Granny.Analyses.StarchArea import StarchArea +from Granny.Models.Images.RGBImage import RGBImage + + +def _get_analysis(): + """Use StarchArea as a concrete implementation of Analysis.""" + return StarchArea() + + +def test_parse_qr_from_filename_valid(): + analysis = _get_analysis() + result = analysis._parse_qr_from_filename( + "APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png" + ) + assert result["project"] == "APPLE2025" + assert result["lot"] == "LOT001" + assert result["date"] == "2025-12-02" + assert result["variety"] == "BB-Late" + + +def test_parse_qr_from_filename_with_path(): + analysis = _get_analysis() + result = analysis._parse_qr_from_filename( + "/some/path/to/APPLE2025_LOT001_2025-12-02_BB-Late_fruit_05.png" + ) + assert result["project"] == "APPLE2025" + assert result["variety"] == "BB-Late" + + +def test_parse_qr_from_filename_jpg(): + analysis = _get_analysis() + result = analysis._parse_qr_from_filename( + "PROJ_LOT_DATE_VAR_fruit_01.jpg" + ) + assert result["project"] == "PROJ" + assert result["variety"] == "VAR" + + +def test_parse_qr_from_filename_legacy(): + """Legacy filenames without QR data should return empty strings.""" + analysis = _get_analysis() + result = analysis._parse_qr_from_filename("apple_fruit_01.png") + assert result["project"] == "" + assert result["lot"] == "" + assert result["date"] == "" + assert result["variety"] == "" + + +def test_parse_qr_from_filename_no_match(): + analysis = _get_analysis() + result = analysis._parse_qr_from_filename("random_image.png") + assert result["project"] == "" + + +def test_add_qr_metadata_valid(): + analysis = _get_analysis() + img = RGBImage("APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png") + analysis._add_qr_metadata(img, "APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png") + + metadata = img.getMetaData() + assert "project" in metadata + assert metadata["project"].getValue() == "APPLE2025" + assert metadata["lot"].getValue() == "LOT001" + assert metadata["date"].getValue() == "2025-12-02" + assert metadata["variety"].getValue() == "BB-Late" + + +def test_add_qr_metadata_legacy(): + """Legacy filenames should not add QR metadata.""" + analysis = _get_analysis() + img = RGBImage("apple_fruit_01.png") + analysis._add_qr_metadata(img, "apple_fruit_01.png") + + metadata = img.getMetaData() + assert "project" not in metadata + assert "lot" not in metadata diff --git a/tests/test_Models/test_Utils/__init__.py b/tests/test_Models/test_Utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_Models/test_Utils/test_QRCodeDetector.py b/tests/test_Models/test_Utils/test_QRCodeDetector.py new file mode 100644 index 0000000..a9a8b84 --- /dev/null +++ b/tests/test_Models/test_Utils/test_QRCodeDetector.py @@ -0,0 +1,97 @@ +import cv2 +import numpy as np +from Granny.Utils.QRCodeDetector import QRCodeDetector + + +def test_instantiation(): + detector = QRCodeDetector() + assert detector.detector is not None + assert isinstance(detector.barcode_enabled, bool) + + +def test_detect_returns_none_for_blank_image(): + detector = QRCodeDetector() + blank = np.zeros((100, 100, 3), dtype=np.uint8) + data, points = detector.detect(blank) + assert data is None + assert points is None + + +def test_detect_barcode_rotation_invariant(): + """Barcode detection should work regardless of image rotation.""" + detector = QRCodeDetector() + if not detector.barcode_enabled: + return + + # Create a test image with a Code128 barcode using pyzbar's expected input + # We'll use a real barcode image if available, otherwise test the rotation logic + # by generating a simple barcode-like pattern + from pyzbar import pyzbar + + # Create a synthetic barcode image using python-barcode if available + try: + import barcode + from barcode.writer import ImageWriter + import tempfile + import os + + code = barcode.get("code128", "TEST123", writer=ImageWriter()) + tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + code.save(tmp.name.replace(".png", "")) + barcode_path = tmp.name.replace(".png", "") + ".png" + + img = cv2.imread(barcode_path) + if img is None: + return + + # Test original orientation + data, points = detector._detect_barcode(img) + assert data == "TEST123" + + # Test 90 degree rotation + rotated = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) + data, points = detector._detect_barcode(rotated) + assert data == "TEST123" + + # Test 180 degree rotation + rotated = cv2.rotate(img, cv2.ROTATE_180) + data, points = detector._detect_barcode(rotated) + assert data == "TEST123" + + # Test 270 degree rotation + rotated = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) + data, points = detector._detect_barcode(rotated) + assert data == "TEST123" + + os.unlink(barcode_path) + except ImportError: + # python-barcode not installed, skip + pass + + +def test_extract_variety_info_pipe_format(): + detector = QRCodeDetector() + info = detector.extract_variety_info("APPLE2026|LOT002|2026-01-23|BB-Early") + assert info["project"] == "APPLE2026" + assert info["lot"] == "LOT002" + assert info["date"] == "2026-01-23" + assert info["full"] == "BB-Early" + assert info["variety"] == "BB" + assert info["timing"] == "Early" + + +def test_extract_variety_info_legacy_format(): + detector = QRCodeDetector() + info = detector.extract_variety_info("BB-Late") + assert info["project"] == "UNKNOWN" + assert info["lot"] == "UNKNOWN" + assert info["full"] == "BB-Late" + assert info["variety"] == "BB" + assert info["timing"] == "Late" + + +def test_extract_variety_info_malformed_pipe(): + detector = QRCodeDetector() + info = detector.extract_variety_info("only|two") + assert info["project"] == "UNKNOWN" + assert info["full"] == "only|two" diff --git a/tests/test_Models/test_Values/test_MetaDataValue.py b/tests/test_Models/test_Values/test_MetaDataValue.py index 25ba651..2117228 100644 --- a/tests/test_Models/test_Values/test_MetaDataValue.py +++ b/tests/test_Models/test_Values/test_MetaDataValue.py @@ -1,5 +1,88 @@ +import os +import tempfile +import pandas as pd from Granny.Models.Values.MetaDataValue import MetaDataValue +from Granny.Models.Values.FloatValue import FloatValue +from Granny.Models.Values.StringValue import StringValue +from Granny.Models.Images.RGBImage import RGBImage +import numpy as np +def _make_image(name, rating_val, project=None, lot=None, date=None, variety=None): + """Helper to create a test image with metadata.""" + img = RGBImage(name) + img.setImage(np.zeros((10, 10, 3), dtype=np.uint8)) - \ No newline at end of file + rating = FloatValue("rating", "rating", "test rating") + rating.setMin(0.0) + rating.setMax(1.0) + rating.setValue(rating_val) + img.addValue(rating) + + if project: + for key, val in [("project", project), ("lot", lot), ("date", date), ("variety", variety)]: + sv = StringValue(key, key, f"test {key}") + sv.setValue(val) + img.addValue(sv) + + return img + + +def test_write_tray_summary_with_string_columns(): + """tray_summary.csv should include string metadata columns like project, lot, date, variety.""" + with tempfile.TemporaryDirectory() as tmpdir: + mdv = MetaDataValue("results", "results", "test") + mdv.setValue(tmpdir) + + images = [ + _make_image("PROJ_LOT1_2025-01-01_VAR_fruit_01.png", 0.8, "PROJ", "LOT1", "2025-01-01", "VAR"), + _make_image("PROJ_LOT1_2025-01-01_VAR_fruit_02.png", 0.6, "PROJ", "LOT1", "2025-01-01", "VAR"), + ] + mdv.setImageList(images) + mdv.writeValue() + + tray_df = pd.read_csv(os.path.join(tmpdir, "tray_summary.csv")) + assert "project" in tray_df.columns + assert "lot" in tray_df.columns + assert "date" in tray_df.columns + assert "variety" in tray_df.columns + assert tray_df["project"].iloc[0] == "PROJ" + assert tray_df["lot"].iloc[0] == "LOT1" + assert tray_df["rating"].iloc[0] == 0.7 # average of 0.8 and 0.6 + + +def test_write_tray_summary_without_string_columns(): + """tray_summary.csv should still work without string metadata.""" + with tempfile.TemporaryDirectory() as tmpdir: + mdv = MetaDataValue("results", "results", "test") + mdv.setValue(tmpdir) + + images = [ + _make_image("apple_fruit_01.png", 0.9), + _make_image("apple_fruit_02.png", 0.7), + ] + mdv.setImageList(images) + mdv.writeValue() + + tray_df = pd.read_csv(os.path.join(tmpdir, "tray_summary.csv")) + assert "TrayName" in tray_df.columns + assert "rating" in tray_df.columns + assert abs(tray_df["rating"].iloc[0] - 0.8) < 0.001 + + +def test_results_csv_has_all_rows(): + """results.csv should have one row per image.""" + with tempfile.TemporaryDirectory() as tmpdir: + mdv = MetaDataValue("results", "results", "test") + mdv.setValue(tmpdir) + + images = [ + _make_image("PROJ_LOT1_2025-01-01_VAR_fruit_01.png", 0.5, "PROJ", "LOT1", "2025-01-01", "VAR"), + _make_image("PROJ_LOT1_2025-01-01_VAR_fruit_02.png", 0.6, "PROJ", "LOT1", "2025-01-01", "VAR"), + _make_image("PROJ_LOT1_2025-01-01_VAR_fruit_03.png", 0.7, "PROJ", "LOT1", "2025-01-01", "VAR"), + ] + mdv.setImageList(images) + mdv.writeValue() + + results_df = pd.read_csv(os.path.join(tmpdir, "results.csv")) + assert len(results_df) == 3