Skip to content

Commit acd392a

Browse files
committed
add tests for - handle None values in ClassifierSettings initialization
1 parent 5d615c7 commit acd392a

File tree

2 files changed

+374
-0
lines changed

2 files changed

+374
-0
lines changed

tests/test_generate_behavior_tables.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,3 +770,50 @@ def test_merge_multiple_behavior_tables_empty_group():
770770

771771
with pytest.raises(ValueError, match="No tables provided for behavior: grooming"):
772772
merge_multiple_behavior_tables(table_groups)
773+
774+
775+
# Integration tests without mocks
776+
777+
778+
def test_classifier_settings_with_none_values_integration():
779+
"""Integration test: ClassifierSettings handles None values correctly.
780+
781+
This is an integration test for issue #45 that verifies the fix works
782+
in the actual codebase without mocking. It tests that None values passed
783+
to ClassifierSettings are correctly converted to defaults and can be
784+
used in comparison operations without raising TypeError.
785+
786+
This test complements the unit tests in test_metadata.py by verifying
787+
the fix works in the context where it was actually failing.
788+
"""
789+
from jabs_postprocess.utils.metadata import (
790+
ClassifierSettings,
791+
DEFAULT_INTERPOLATE,
792+
DEFAULT_STITCH,
793+
DEFAULT_MIN_BOUT,
794+
)
795+
from jabs_postprocess.utils.project_utils import Bouts
796+
797+
# Create settings with None values (simulates the bug scenario)
798+
settings = ClassifierSettings(
799+
behavior="grooming",
800+
interpolate=None,
801+
stitch=None,
802+
min_bout=None,
803+
)
804+
805+
# Verify defaults are applied
806+
assert settings.interpolate == DEFAULT_INTERPOLATE
807+
assert settings.stitch == DEFAULT_STITCH
808+
assert settings.min_bout == DEFAULT_MIN_BOUT
809+
810+
# Create some bout data to test filter_by_settings
811+
# This simulates the actual code path where the bug occurred
812+
bouts = Bouts.from_value_vector([0, 0, 1, 1, 1, 0, 0, -1, -1, 0, 1, 1])
813+
814+
# This should not raise TypeError
815+
# Previously would fail with: TypeError: '>' not supported between instances of 'NoneType' and 'int'
816+
try:
817+
bouts.filter_by_settings(settings)
818+
except TypeError as e:
819+
pytest.fail(f"filter_by_settings raised TypeError with None values: {e}")

tests/utils/test_metadata.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
"""Unit tests for the metadata module.
2+
3+
This test module validates the functionality of metadata classes, particularly
4+
ClassifierSettings and its handling of None values for optional parameters.
5+
"""
6+
7+
import pytest
8+
9+
from jabs_postprocess.utils.metadata import (
10+
ClassifierSettings,
11+
FeatureSettings,
12+
DEFAULT_INTERPOLATE,
13+
DEFAULT_STITCH,
14+
DEFAULT_MIN_BOUT,
15+
)
16+
17+
18+
class TestClassifierSettingsInitialization:
19+
"""Test ClassifierSettings initialization with various parameter combinations."""
20+
21+
def test_all_explicit_values(self):
22+
"""Test ClassifierSettings with all parameters explicitly set."""
23+
settings = ClassifierSettings(
24+
behavior="grooming",
25+
interpolate=10,
26+
stitch=15,
27+
min_bout=20,
28+
)
29+
30+
assert settings.behavior == "grooming"
31+
assert settings.interpolate == 10
32+
assert settings.stitch == 15
33+
assert settings.min_bout == 20
34+
35+
def test_all_none_values_use_defaults(self):
36+
"""Test that None values default to constants.
37+
38+
This is the core fix for issue #45 - None values should be converted
39+
to their default constants rather than remaining None.
40+
"""
41+
settings = ClassifierSettings(
42+
behavior="grooming",
43+
interpolate=None,
44+
stitch=None,
45+
min_bout=None,
46+
)
47+
48+
assert settings.behavior == "grooming"
49+
assert settings.interpolate == DEFAULT_INTERPOLATE
50+
assert settings.stitch == DEFAULT_STITCH
51+
assert settings.min_bout == DEFAULT_MIN_BOUT
52+
53+
def test_mixed_none_and_explicit_values(self):
54+
"""Test ClassifierSettings with some None and some explicit values."""
55+
settings = ClassifierSettings(
56+
behavior="walking",
57+
interpolate=10,
58+
stitch=None,
59+
min_bout=25,
60+
)
61+
62+
assert settings.behavior == "walking"
63+
assert settings.interpolate == 10
64+
assert settings.stitch == DEFAULT_STITCH # Should use default
65+
assert settings.min_bout == 25
66+
67+
def test_zero_values_are_preserved(self):
68+
"""Test that explicit zero values are not treated as None."""
69+
settings = ClassifierSettings(
70+
behavior="feeding",
71+
interpolate=0,
72+
stitch=0,
73+
min_bout=0,
74+
)
75+
76+
assert settings.interpolate == 0
77+
assert settings.stitch == 0
78+
assert settings.min_bout == 0
79+
80+
@pytest.mark.parametrize(
81+
"interpolate,stitch,min_bout",
82+
[
83+
(None, None, None),
84+
(5, None, None),
85+
(None, 10, None),
86+
(None, None, 15),
87+
(5, 10, None),
88+
(5, None, 15),
89+
(None, 10, 15),
90+
(5, 10, 15),
91+
],
92+
)
93+
def test_all_parameter_combinations(self, interpolate, stitch, min_bout):
94+
"""Test all combinations of None and explicit values."""
95+
settings = ClassifierSettings(
96+
behavior="test",
97+
interpolate=interpolate,
98+
stitch=stitch,
99+
min_bout=min_bout,
100+
)
101+
102+
# Verify each parameter is either the provided value or the default
103+
expected_interpolate = (
104+
interpolate if interpolate is not None else DEFAULT_INTERPOLATE
105+
)
106+
expected_stitch = stitch if stitch is not None else DEFAULT_STITCH
107+
expected_min_bout = min_bout if min_bout is not None else DEFAULT_MIN_BOUT
108+
109+
assert settings.interpolate == expected_interpolate
110+
assert settings.stitch == expected_stitch
111+
assert settings.min_bout == expected_min_bout
112+
113+
114+
class TestClassifierSettingsComparison:
115+
"""Test that ClassifierSettings values work in comparison operations.
116+
117+
These tests verify the fix for issue #45 - the bug occurred when
118+
None values were compared with integers in filter_by_settings().
119+
"""
120+
121+
def test_comparison_with_defaults_from_none(self):
122+
"""Test that default values from None can be compared with integers."""
123+
settings = ClassifierSettings(
124+
behavior="test",
125+
interpolate=None,
126+
stitch=None,
127+
min_bout=None,
128+
)
129+
130+
# These comparisons would raise TypeError if values were None
131+
assert settings.interpolate > 0
132+
assert settings.stitch > 0
133+
assert settings.min_bout > 0
134+
assert settings.interpolate >= 0
135+
assert settings.stitch <= 100
136+
assert settings.min_bout == DEFAULT_MIN_BOUT
137+
138+
def test_comparison_with_explicit_values(self):
139+
"""Test that explicit values work in comparisons."""
140+
settings = ClassifierSettings(
141+
behavior="test",
142+
interpolate=10,
143+
stitch=15,
144+
min_bout=20,
145+
)
146+
147+
assert settings.interpolate > 5
148+
assert settings.stitch > 10
149+
assert settings.min_bout > 15
150+
assert settings.interpolate == 10
151+
assert settings.stitch == 15
152+
assert settings.min_bout == 20
153+
154+
def test_comparison_with_zero_values(self):
155+
"""Test that zero values work in comparisons."""
156+
settings = ClassifierSettings(
157+
behavior="test",
158+
interpolate=0,
159+
stitch=0,
160+
min_bout=0,
161+
)
162+
163+
assert settings.interpolate >= 0
164+
assert settings.stitch >= 0
165+
assert settings.min_bout >= 0
166+
assert not (settings.interpolate > 0)
167+
assert not (settings.stitch > 0)
168+
assert not (settings.min_bout > 0)
169+
170+
171+
class TestClassifierSettingsProperties:
172+
"""Test ClassifierSettings property accessors."""
173+
174+
def test_behavior_property(self):
175+
"""Test that behavior property returns the correct value."""
176+
settings = ClassifierSettings("grooming", None, None, None)
177+
assert settings.behavior == "grooming"
178+
179+
def test_interpolate_property(self):
180+
"""Test that interpolate property returns the correct value."""
181+
settings = ClassifierSettings("test", 10, None, None)
182+
assert settings.interpolate == 10
183+
184+
def test_stitch_property(self):
185+
"""Test that stitch property returns the correct value."""
186+
settings = ClassifierSettings("test", None, 15, None)
187+
assert settings.stitch == 15
188+
189+
def test_min_bout_property(self):
190+
"""Test that min_bout property returns the correct value."""
191+
settings = ClassifierSettings("test", None, None, 20)
192+
assert settings.min_bout == 20
193+
194+
def test_all_properties_with_none(self):
195+
"""Test that all properties return defaults when initialized with None."""
196+
settings = ClassifierSettings("test", None, None, None)
197+
assert settings.behavior == "test"
198+
assert settings.interpolate == DEFAULT_INTERPOLATE
199+
assert settings.stitch == DEFAULT_STITCH
200+
assert settings.min_bout == DEFAULT_MIN_BOUT
201+
202+
203+
class TestClassifierSettingsStringRepresentation:
204+
"""Test ClassifierSettings string methods."""
205+
206+
def test_str_with_explicit_values(self):
207+
"""Test __str__ with explicit values."""
208+
settings = ClassifierSettings("grooming", 10, 15, 20)
209+
result = str(settings)
210+
211+
assert "grooming" in result
212+
assert "10" in result
213+
assert "15" in result
214+
assert "20" in result
215+
216+
def test_str_with_none_values_shows_defaults(self):
217+
"""Test __str__ with None values shows defaults."""
218+
settings = ClassifierSettings("walking", None, None, None)
219+
result = str(settings)
220+
221+
assert "walking" in result
222+
assert str(DEFAULT_INTERPOLATE) in result
223+
assert str(DEFAULT_STITCH) in result
224+
assert str(DEFAULT_MIN_BOUT) in result
225+
226+
def test_repr_equals_str(self):
227+
"""Test that __repr__ returns the same as __str__."""
228+
settings = ClassifierSettings("test", 5, 10, 15)
229+
assert repr(settings) == str(settings)
230+
231+
232+
class TestFeatureSettingsInheritance:
233+
"""Test that FeatureSettings inherits the None-handling behavior.
234+
235+
FeatureSettings extends ClassifierSettings and should benefit from
236+
the same None-handling fix.
237+
"""
238+
239+
@pytest.fixture
240+
def temp_config_file(self, tmp_path):
241+
"""Create a temporary YAML config file for testing."""
242+
config_file = tmp_path / "test_config.yaml"
243+
config_content = """
244+
behavior: test_behavior
245+
definition:
246+
- feature1 > 10
247+
- feature2 < 5
248+
"""
249+
config_file.write_text(config_content)
250+
return config_file
251+
252+
def test_feature_settings_with_none_values(self, temp_config_file):
253+
"""Test FeatureSettings with None values uses defaults."""
254+
settings = FeatureSettings(
255+
config_file=str(temp_config_file),
256+
interpolate=None,
257+
stitch=None,
258+
min_bout=None,
259+
)
260+
261+
# Should inherit None-handling from ClassifierSettings
262+
assert settings.interpolate == DEFAULT_INTERPOLATE
263+
assert settings.stitch == DEFAULT_STITCH
264+
assert settings.min_bout == DEFAULT_MIN_BOUT
265+
266+
def test_feature_settings_with_explicit_values(self, temp_config_file):
267+
"""Test FeatureSettings with explicit values overrides defaults."""
268+
settings = FeatureSettings(
269+
config_file=str(temp_config_file),
270+
interpolate=10,
271+
stitch=15,
272+
min_bout=20,
273+
)
274+
275+
assert settings.interpolate == 10
276+
assert settings.stitch == 15
277+
assert settings.min_bout == 20
278+
279+
def test_feature_settings_config_file_defaults(self, tmp_path):
280+
"""Test that FeatureSettings can get defaults from config file."""
281+
config_file = tmp_path / "test_config.yaml"
282+
config_content = """
283+
behavior: test_behavior
284+
definition:
285+
- feature1 > 10
286+
interpolate: 7
287+
stitch: 12
288+
min_bout: 18
289+
"""
290+
config_file.write_text(config_content)
291+
292+
settings = FeatureSettings(
293+
config_file=str(config_file),
294+
interpolate=None,
295+
stitch=None,
296+
min_bout=None,
297+
)
298+
299+
# Should use values from config file
300+
assert settings.interpolate == 7
301+
assert settings.stitch == 12
302+
assert settings.min_bout == 18
303+
304+
def test_feature_settings_explicit_overrides_config(self, tmp_path):
305+
"""Test that explicit values override config file values."""
306+
config_file = tmp_path / "test_config.yaml"
307+
config_content = """
308+
behavior: test_behavior
309+
definition:
310+
- feature1 > 10
311+
interpolate: 7
312+
stitch: 12
313+
min_bout: 18
314+
"""
315+
config_file.write_text(config_content)
316+
317+
settings = FeatureSettings(
318+
config_file=str(config_file),
319+
interpolate=100,
320+
stitch=200,
321+
min_bout=300,
322+
)
323+
324+
# Explicit values should override config file
325+
assert settings.interpolate == 100
326+
assert settings.stitch == 200
327+
assert settings.min_bout == 300

0 commit comments

Comments
 (0)