From 9d8fc79e46861eedb3927eec4bd90387414212c3 Mon Sep 17 00:00:00 2001 From: AdenAthar Date: Fri, 3 Apr 2026 15:19:33 -0700 Subject: [PATCH] Expand test suite from 92 to 156 tests - BlushColor: add 12 tests covering defaults, _calculateBlush shape/ratio/threshold - PeelColor: add 12 tests covering defaults, remove_purple, calculate_bin_distance - SuperficialScald: add 14 tests covering defaults, _smoothMask, _calculateScald, _removeTrayResidue - Segmentation: add 9 tests covering defaults, text color params, QR detector init - QRCodeDetector: add 8 tests covering variety info parsing edge cases - BoolValue: add 8 tests covering get/set/type/validate; fix validate() signature to accept optional value argument --- Granny/Models/Values/BoolValue.py | 2 +- tests/test_Analyses/test_BlushColor.py | 96 ++++++++++++++- tests/test_Analyses/test_PeelColor.py | 92 +++++++++++++- tests/test_Analyses/test_Segmentation.py | 63 +++++++++- tests/test_Analyses/test_SuperficialScald.py | 113 +++++++++++++++++- .../test_Utils/test_QRCodeDetector.py | 42 +++++++ .../test_Models/test_Values/test_BoolValue.py | 53 +++++++- 7 files changed, 451 insertions(+), 10 deletions(-) diff --git a/Granny/Models/Values/BoolValue.py b/Granny/Models/Values/BoolValue.py index 34ec38a..372a0fa 100644 --- a/Granny/Models/Values/BoolValue.py +++ b/Granny/Models/Values/BoolValue.py @@ -13,7 +13,7 @@ def __init__(self, name: str, label: str, help: str): super().__init__(name, label, help) self.type = bool - def validate(self) -> bool: + def validate(self, value=None) -> bool: """ {@inheritdoc} """ diff --git a/tests/test_Analyses/test_BlushColor.py b/tests/test_Analyses/test_BlushColor.py index 08805b9..5385447 100644 --- a/tests/test_Analyses/test_BlushColor.py +++ b/tests/test_Analyses/test_BlushColor.py @@ -1,15 +1,107 @@ +import numpy as np +import pytest from Granny.Analyses.BlushColor import BlushColor def test_BlushColorInstantiation(): - """Test that BlushColor can be instantiated""" analysis = BlushColor() assert analysis is not None assert analysis.__analysis_name__ == "blush" def test_BlushColorInputImages(): - """Test that BlushColor input_images can be set""" analysis = BlushColor() analysis.input_images.setValue("demo/pear_images/full_masked_images") assert analysis.input_images.getValue() == "demo/pear_images/full_masked_images" + + +# --------------------------------------------------------------------------- +# Default parameter values +# --------------------------------------------------------------------------- + +def test_default_threshold_is_148(): + assert BlushColor().threshold.getValue() == 148 + + +def test_default_fruit_threshold_is_140(): + assert BlushColor().fruit_threshold.getValue() == 140 + + +def test_default_blush_color(): + a = BlushColor() + assert a.blush_color_r.getValue() == 150 + assert a.blush_color_g.getValue() == 55 + assert a.blush_color_b.getValue() == 50 + + +def test_default_text_position(): + a = BlushColor() + assert a.text_x.getValue() == 20 + assert a.text_y.getValue() == 50 + + +def test_default_font_scale_is_1(): + assert BlushColor().font_scale.getValue() == pytest.approx(1.0) + + +def test_default_text_thickness_is_3(): + assert BlushColor().text_thickness.getValue() == 3 + + +def test_threshold_boundary_values(): + a = BlushColor() + a.threshold.setValue(0) + assert a.threshold.getValue() == 0 + a.threshold.setValue(255) + assert a.threshold.getValue() == 255 + + +# --------------------------------------------------------------------------- +# _calculateBlush with synthetic images +# --------------------------------------------------------------------------- + +def _make_bgr(r, g, b, size=50): + img = np.zeros((size, size, 3), dtype=np.uint8) + img[:, :, 0] = b + img[:, :, 1] = g + img[:, :, 2] = r + return img + + +def test_calculate_blush_returns_ratio_between_0_and_1(): + a = BlushColor() + img = _make_bgr(200, 100, 100) + ratio, _ = a._calculateBlush(img) + assert 0.0 <= ratio <= 1.0 + + +def test_calculate_blush_output_shape_matches_input(): + a = BlushColor() + img = _make_bgr(200, 100, 100) + _, result = a._calculateBlush(img) + assert result.shape == img.shape + + +def test_calculate_blush_threshold_affects_ratio(): + img = _make_bgr(200, 150, 100) + low_a = BlushColor() + low_a.threshold.setValue(50) + high_a = BlushColor() + high_a.threshold.setValue(200) + low_ratio, _ = low_a._calculateBlush(img) + high_ratio, _ = high_a._calculateBlush(img) + assert low_ratio != high_ratio + + +def test_calculate_blush_all_black_image(): + a = BlushColor() + img = np.zeros((50, 50, 3), dtype=np.uint8) + ratio, result = a._calculateBlush(img) + assert result.shape == img.shape + + +def test_calculate_blush_returns_float(): + a = BlushColor() + img = _make_bgr(180, 100, 80) + ratio, _ = a._calculateBlush(img) + assert isinstance(ratio, float) diff --git a/tests/test_Analyses/test_PeelColor.py b/tests/test_Analyses/test_PeelColor.py index 78e4824..6664d9d 100644 --- a/tests/test_Analyses/test_PeelColor.py +++ b/tests/test_Analyses/test_PeelColor.py @@ -1,15 +1,103 @@ +import numpy as np +import pytest from Granny.Analyses.PeelColor import PeelColor def test_PeelColorInstantiation(): - """Test that PeelColor can be instantiated""" analysis = PeelColor() assert analysis is not None assert analysis.__analysis_name__ == "color" def test_PeelColorInputImages(): - """Test that PeelColor input_images can be set""" analysis = PeelColor() analysis.input_images.setValue("demo/pear_images/full_masked_images") assert analysis.input_images.getValue() == "demo/pear_images/full_masked_images" + + +# --------------------------------------------------------------------------- +# Default parameter values +# --------------------------------------------------------------------------- + +def test_default_purple_threshold_is_126(): + assert PeelColor().purple_threshold.getValue() == 126 + + +def test_default_lightness_range(): + a = PeelColor() + assert a.lightness_min.getValue() == 0 + assert a.lightness_max.getValue() == 255 + + +def test_default_green_range(): + a = PeelColor() + assert a.green_min.getValue() == 0 + assert a.green_max.getValue() == 128 + + +def test_default_yellow_range(): + a = PeelColor() + assert a.yellow_min.getValue() == 128 + assert a.yellow_max.getValue() == 255 + + +def test_default_normalize_lightness_is_50(): + assert PeelColor().normalize_lightness.getValue() == 50 + + +def test_mean_values_length_match(): + a = PeelColor() + assert len(a.MEAN_VALUES_A) == len(a.MEAN_VALUES_B) == len(a.SCORE) + + +def test_line_points_are_2d(): + a = PeelColor() + assert a.LINE_POINT_1.shape == (2,) + assert a.LINE_POINT_2.shape == (2,) + + +# --------------------------------------------------------------------------- +# remove_purple +# --------------------------------------------------------------------------- + +def test_remove_purple_output_shape(): + a = PeelColor() + img = np.full((50, 50, 3), 128, dtype=np.uint8) + result = a.remove_purple(img) + assert result.shape == img.shape + + +def test_remove_purple_does_not_modify_original(): + a = PeelColor() + img = np.full((50, 50, 3), 100, dtype=np.uint8) + original = img.copy() + a.remove_purple(img) + np.testing.assert_array_equal(img, original) + + +# --------------------------------------------------------------------------- +# calculate_bin_distance +# --------------------------------------------------------------------------- + +def test_calculate_bin_distance_euclidean_returns_valid_bin(): + a = PeelColor() + bin_num, dist = a.calculate_bin_distance([-20.0, 70.0], method="Euclidean") + assert 1 <= bin_num <= len(a.SCORE) + + +def test_calculate_bin_distance_score_method(): + a = PeelColor() + bin_num, dist = a.calculate_bin_distance([0.6], method="Score") + assert 1 <= bin_num <= len(a.SCORE) + + +def test_calculate_bin_distance_x_component(): + a = PeelColor() + bin_num, _ = a.calculate_bin_distance([-20.0, 70.0], method="X-component") + assert 1 <= bin_num <= len(a.SCORE) + + +def test_calculate_bin_distance_y_component(): + a = PeelColor() + bin_num, _ = a.calculate_bin_distance([-20.0, 70.0], method="Y-component") + assert 1 <= bin_num <= len(a.SCORE) diff --git a/tests/test_Analyses/test_Segmentation.py b/tests/test_Analyses/test_Segmentation.py index b1a8bae..37c2e0c 100644 --- a/tests/test_Analyses/test_Segmentation.py +++ b/tests/test_Analyses/test_Segmentation.py @@ -1,5 +1,66 @@ +import pytest from Granny.Analyses.Segmentation import Segmentation + def test_performAnalysis(): analysis = Segmentation() - \ No newline at end of file + + +# --------------------------------------------------------------------------- +# Default parameter values +# --------------------------------------------------------------------------- + +def test_default_conf_threshold_is_0_7(): + assert Segmentation().conf_threshold.getValue() == pytest.approx(0.7) + + +def test_default_iou_threshold(): + assert Segmentation().iou_threshold.getValue() == pytest.approx(0.45) + + +def test_default_font_scale(): + s = Segmentation() + assert s.font_scale.getValue() == pytest.approx(2.0) + + +def test_default_text_thickness_is_3(): + assert Segmentation().text_thickness.getValue() == 3 + + +def test_default_text_color_is_black(): + s = Segmentation() + assert s.text_color_r.getValue() == 0 + assert s.text_color_g.getValue() == 0 + assert s.text_color_b.getValue() == 0 + + +def test_text_color_boundary_values(): + s = Segmentation() + s.text_color_r.setValue(255) + s.text_color_g.setValue(255) + s.text_color_b.setValue(255) + assert s.text_color_r.getValue() == 255 + assert s.text_color_g.getValue() == 255 + assert s.text_color_b.getValue() == 255 + + +def test_conf_threshold_range(): + s = Segmentation() + s.conf_threshold.setValue(0.0) + assert s.conf_threshold.getValue() == pytest.approx(0.0) + s.conf_threshold.setValue(1.0) + assert s.conf_threshold.getValue() == pytest.approx(1.0) + + +def test_qr_detector_initialized(): + from Granny.Utils.QRCodeDetector import QRCodeDetector + s = Segmentation() + assert isinstance(s.qr_detector, QRCodeDetector) + + +def test_variety_info_initially_none(): + assert Segmentation().variety_info is None + + +def test_analysis_name_is_segmentation(): + assert Segmentation().__analysis_name__ == "segmentation" diff --git a/tests/test_Analyses/test_SuperficialScald.py b/tests/test_Analyses/test_SuperficialScald.py index 5efeea1..ef632a6 100644 --- a/tests/test_Analyses/test_SuperficialScald.py +++ b/tests/test_Analyses/test_SuperficialScald.py @@ -1,15 +1,124 @@ +import numpy as np +import pytest from Granny.Analyses.SuperficialScald import SuperficialScald def test_SuperficialScaldInstantiation(): - """Test that SuperficialScald can be instantiated""" analysis = SuperficialScald() assert analysis is not None assert analysis.__analysis_name__ == "scald" def test_SuperficialScaldInputImages(): - """Test that SuperficialScald input_images can be set""" analysis = SuperficialScald() analysis.input_images.setValue("demo/granny_smith_images/full_masked_images") assert analysis.input_images.getValue() == "demo/granny_smith_images/full_masked_images" + + +# --------------------------------------------------------------------------- +# Default parameter values +# --------------------------------------------------------------------------- + +def test_default_morph_kernel_is_10(): + assert SuperficialScald().morph_kernel.getValue() == 10 + + +def test_default_min_threshold_is_100(): + assert SuperficialScald().min_threshold.getValue() == 100 + + +def test_default_purple_threshold_is_126(): + assert SuperficialScald().purple_threshold.getValue() == 126 + + +def test_default_blur_kernel_is_3(): + assert SuperficialScald().blur_kernel.getValue() == 3 + + +def test_default_hist_factor(): + assert SuperficialScald().hist_factor.getValue() == pytest.approx(0.333) + + +def test_default_hist_top_n_is_10(): + assert SuperficialScald().hist_top_n.getValue() == 10 + + +# --------------------------------------------------------------------------- +# _smoothMask +# --------------------------------------------------------------------------- + +def test_smooth_mask_output_shape(): + a = SuperficialScald() + mask = np.ones((100, 100), dtype=np.uint8) + result = a._smoothMask(mask) + assert result.shape == mask.shape + + +def test_smooth_mask_all_zeros_stays_zero(): + a = SuperficialScald() + mask = np.zeros((100, 100), dtype=np.uint8) + result = a._smoothMask(mask) + assert result.sum() == 0 + + +def test_smooth_mask_all_ones_stays_ones(): + a = SuperficialScald() + mask = np.ones((100, 100), dtype=np.uint8) + result = a._smoothMask(mask) + assert result.sum() > 0 + + +# --------------------------------------------------------------------------- +# _calculateScald +# --------------------------------------------------------------------------- + +def test_calculate_scald_full_mask_returns_zero(): + a = SuperficialScald() + img = np.full((50, 50, 3), 128, dtype=np.uint8) + bw = img.copy() + score = a._calculateScald(bw, img) + assert score == pytest.approx(0.0) + + +def test_calculate_scald_empty_mask_returns_one(): + a = SuperficialScald() + img = np.full((50, 50, 3), 128, dtype=np.uint8) + bw = np.zeros((50, 50, 3), dtype=np.uint8) + score = a._calculateScald(bw, img) + assert score == pytest.approx(1.0) + + +def test_calculate_scald_returns_value_between_0_and_1(): + a = SuperficialScald() + img = np.full((50, 50, 3), 128, dtype=np.uint8) + bw = img.copy() + bw[:25, :] = 0 + score = a._calculateScald(bw, img) + assert 0.0 <= score <= 1.0 + + +def test_calculate_scald_zero_ground_area_returns_one(): + a = SuperficialScald() + img = np.zeros((50, 50, 3), dtype=np.uint8) + bw = np.zeros((50, 50, 3), dtype=np.uint8) + score = a._calculateScald(bw, img) + assert score == 1 + + +# --------------------------------------------------------------------------- +# _removeTrayResidue +# --------------------------------------------------------------------------- + +def test_remove_tray_residue_output_shape(): + a = SuperficialScald() + img = np.full((50, 50, 3), 100, dtype=np.uint8) + result = a._removeTrayResidue(img) + assert result.shape == img.shape + + +def test_remove_tray_residue_does_not_modify_input(): + a = SuperficialScald() + img = np.full((50, 50, 3), 100, dtype=np.uint8) + original = img.copy() + a._removeTrayResidue(img) + np.testing.assert_array_equal(img, original) diff --git a/tests/test_Models/test_Utils/test_QRCodeDetector.py b/tests/test_Models/test_Utils/test_QRCodeDetector.py index a9a8b84..b06e4f9 100644 --- a/tests/test_Models/test_Utils/test_QRCodeDetector.py +++ b/tests/test_Models/test_Utils/test_QRCodeDetector.py @@ -95,3 +95,45 @@ def test_extract_variety_info_malformed_pipe(): info = detector.extract_variety_info("only|two") assert info["project"] == "UNKNOWN" assert info["full"] == "only|two" + + +def test_extract_variety_info_raw_field_always_set(): + detector = QRCodeDetector() + raw = "PROJ|LOT|2026-01-01|HC-Late" + info = detector.extract_variety_info(raw) + assert info["raw"] == raw + + +def test_extract_variety_info_no_timing(): + detector = QRCodeDetector() + info = detector.extract_variety_info("HONEYCRISP") + assert info["variety"] == "HONEYCRISP" + assert info["timing"] == "" + + +def test_extract_variety_info_pipe_variety_no_dash(): + detector = QRCodeDetector() + info = detector.extract_variety_info("P|L|D|HONEYCRISP") + assert info["variety"] == "HONEYCRISP" + assert info["timing"] == "" + + +def test_detect_grayscale_image_does_not_crash(): + detector = QRCodeDetector() + gray = np.zeros((100, 100), dtype=np.uint8) + if detector.barcode_enabled: + data, points = detector._detect_barcode(gray) + assert data is None + + +def test_detect_returns_two_values(): + detector = QRCodeDetector() + result = detector.detect(np.zeros((100, 100, 3), dtype=np.uint8)) + assert len(result) == 2 + + +def test_extract_variety_info_three_pipe_parts(): + detector = QRCodeDetector() + info = detector.extract_variety_info("A|B|C") + assert info["project"] == "UNKNOWN" + assert info["lot"] == "UNKNOWN" diff --git a/tests/test_Models/test_Values/test_BoolValue.py b/tests/test_Models/test_Values/test_BoolValue.py index 943ca3d..34c1500 100644 --- a/tests/test_Models/test_Values/test_BoolValue.py +++ b/tests/test_Models/test_Values/test_BoolValue.py @@ -1,6 +1,55 @@ from Granny.Models.Values.BoolValue import BoolValue -# Checks if the value was properly set + def test_validate(): value_1 = BoolValue("name", "label", "help") - assert value_1.validate() is True \ No newline at end of file + assert value_1.validate() is True + + +def test_name_label_help(): + v = BoolValue("myname", "mylabel", "myhelp") + assert v.getName() == "myname" + assert v.getLabel() == "mylabel" + assert v.getHelp() == "myhelp" + + +def test_set_get_true(): + v = BoolValue("b", "b", "b") + v.setValue(True) + assert v.getValue() is True + + +def test_set_get_false(): + v = BoolValue("b", "b", "b") + v.setValue(False) + assert v.getValue() is False + + +def test_type_is_bool(): + v = BoolValue("b", "b", "b") + assert v.type is bool + + +def test_overwrite_value(): + v = BoolValue("b", "b", "b") + v.setValue(True) + v.setValue(False) + assert v.getValue() is False + + +def test_validate_with_value(): + v = BoolValue("b", "b", "b") + assert v.validate(True) is True + assert v.validate(False) is True + + +def test_set_is_required(): + v = BoolValue("b", "b", "b") + v.setIsRequired(True) + assert v.getIsRequired() is True + + +def test_set_is_required_false(): + v = BoolValue("b", "b", "b") + v.setIsRequired(False) + assert v.getIsRequired() is False \ No newline at end of file