66- Arrow-direct extraction (no pandas)
77- Deduplication and sorting
88- Error classification and propagation
9+ - Wide schema (default) vs narrow schema (legacy, deprecated)
910"""
1011
1112import unittest
13+ import warnings
1214from datetime import datetime
1315from unittest .mock import MagicMock , patch
1416import sys
1921from slicks .discovery import discover_sensors
2022
2123
22- class TestDiscoverSensorsMocked (unittest .TestCase ):
23- """Tests for discover_sensors with mocked InfluxDB."""
24+ def _make_wide_mock_config (mock_config ):
25+ """Set config attributes expected by the wide schema path."""
26+ mock_config .INFLUX_SCHEMA = "iox"
27+ mock_config .INFLUX_TABLE = "WFR26"
28+ mock_config .INFLUX_DB = "WFR26"
29+
30+
31+ class TestDiscoverSensorsWide (unittest .TestCase ):
32+ """Tests for discover_sensors with wide schema (default)."""
2433
2534 @patch ('slicks.discovery.get_influx_client' )
2635 @patch ('slicks.discovery.config' )
27- def test_returns_sorted_unique_sensors (self , mock_config , mock_client_class ):
28- """Should return sorted deduplicated sensor list."""
29- mock_config .INFLUX_URL = "http://localhost:8086"
30- mock_config .INFLUX_TOKEN = "test-token"
31- mock_config .INFLUX_DB = "test-db"
36+ def test_returns_sorted_sensors (self , mock_config , mock_client_class ):
37+ """Should return sorted sensor list from column metadata."""
38+ _make_wide_mock_config (mock_config )
3239
33- mock_col_values = [MagicMock (), MagicMock (), MagicMock () ]
40+ mock_col_values = [MagicMock (), MagicMock ()]
3441 mock_col_values [0 ].as_py .return_value = "SensorB"
3542 mock_col_values [1 ].as_py .return_value = "SensorA"
36- mock_col_values [2 ].as_py .return_value = "SensorB" # duplicate
3743
3844 mock_table = MagicMock ()
39- mock_table .num_rows = 3
45+ mock_table .num_rows = 2
4046 mock_col = MagicMock ()
4147 mock_col .__iter__ = MagicMock (return_value = iter (mock_col_values ))
4248 mock_table .column .return_value = mock_col
4349
4450 mock_client = MagicMock ()
4551 mock_client .query .return_value = mock_table
46- mock_client .close = MagicMock ()
4752 mock_client_class .return_value = mock_client
4853
4954 result = discover_sensors (
@@ -57,16 +62,13 @@ def test_returns_sorted_unique_sensors(self, mock_config, mock_client_class):
5762 @patch ('slicks.discovery.get_influx_client' )
5863 @patch ('slicks.discovery.config' )
5964 def test_returns_empty_list_when_no_data (self , mock_config , mock_client_class ):
60- """Should return empty list when no sensors found."""
61- mock_config .INFLUX_URL = "http://localhost:8086"
62- mock_config .INFLUX_TOKEN = "test-token"
63- mock_config .INFLUX_DB = "test-db"
65+ """Should return empty list when no columns found."""
66+ _make_wide_mock_config (mock_config )
6467
6568 mock_table = MagicMock ()
6669 mock_table .num_rows = 0
6770 mock_client = MagicMock ()
6871 mock_client .query .return_value = mock_table
69- mock_client .close = MagicMock ()
7072 mock_client_class .return_value = mock_client
7173
7274 result = discover_sensors (
@@ -77,33 +79,11 @@ def test_returns_empty_list_when_no_data(self, mock_config, mock_client_class):
7779
7880 self .assertEqual (result , [])
7981
80- @patch ('slicks.discovery.get_influx_client' )
81- @patch ('slicks.discovery.config' )
82- def test_raises_on_permanent_error (self , mock_config , mock_client_class ):
83- """Should raise RuntimeError on auth failure."""
84- mock_config .INFLUX_URL = "http://localhost:8086"
85- mock_config .INFLUX_TOKEN = "bad-token"
86- mock_config .INFLUX_DB = "test-db"
87-
88- mock_client = MagicMock ()
89- mock_client .query .side_effect = Exception ("Unauthorized: invalid token" )
90- mock_client .close = MagicMock ()
91- mock_client_class .return_value = mock_client
92-
93- with self .assertRaises (RuntimeError ):
94- discover_sensors (
95- start_time = datetime (2025 , 1 , 1 ),
96- end_time = datetime (2025 , 1 , 2 ),
97- show_progress = False ,
98- )
99-
10082 @patch ('slicks.discovery.get_influx_client' )
10183 @patch ('slicks.discovery.config' )
10284 def test_filters_none_values (self , mock_config , mock_client_class ):
103- """Should skip None values in the signal name column."""
104- mock_config .INFLUX_URL = "http://localhost:8086"
105- mock_config .INFLUX_TOKEN = "test-token"
106- mock_config .INFLUX_DB = "test-db"
85+ """Should skip None values in the column_name column."""
86+ _make_wide_mock_config (mock_config )
10787
10888 mock_col_values = [MagicMock (), MagicMock ()]
10989 mock_col_values [0 ].as_py .return_value = "SensorA"
@@ -117,7 +97,6 @@ def test_filters_none_values(self, mock_config, mock_client_class):
11797
11898 mock_client = MagicMock ()
11999 mock_client .query .return_value = mock_table
120- mock_client .close = MagicMock ()
121100 mock_client_class .return_value = mock_client
122101
123102 result = discover_sensors (
@@ -130,53 +109,126 @@ def test_filters_none_values(self, mock_config, mock_client_class):
130109
131110 @patch ('slicks.discovery.get_influx_client' )
132111 @patch ('slicks.discovery.config' )
133- def test_default_chunk_size_is_7_days (self , mock_config , mock_client_class ):
134- """Default chunk_size_days=7 should produce 2 chunks for 10-day range."""
135- mock_config .INFLUX_URL = "http://localhost:8086"
136- mock_config .INFLUX_TOKEN = "test-token"
137- mock_config .INFLUX_DB = "test-db"
112+ def test_single_query_regardless_of_time_range (self , mock_config , mock_client_class ):
113+ """Wide schema should issue exactly one metadata query, ignoring time range."""
114+ _make_wide_mock_config (mock_config )
138115
139116 mock_table = MagicMock ()
140117 mock_table .num_rows = 0
141118 mock_client = MagicMock ()
142119 mock_client .query .return_value = mock_table
143- mock_client .close = MagicMock ()
144120 mock_client_class .return_value = mock_client
145121
146122 discover_sensors (
147123 start_time = datetime (2025 , 1 , 1 ),
148- end_time = datetime (2025 , 1 , 11 ),
124+ end_time = datetime (2025 , 12 , 31 ),
149125 show_progress = False ,
150126 )
151127
152- # 10-day range with 7-day chunks = 2 chunks, each triggers at least 1 query
153- self .assertGreaterEqual (mock_client .query .call_count , 2 )
128+ self .assertEqual (mock_client .query .call_count , 1 )
154129
155130 @patch ('slicks.discovery.get_influx_client' )
156131 @patch ('slicks.discovery.config' )
157132 def test_backward_compatible_client_param (self , mock_config , mock_client_class ):
158133 """Passing client= should not raise (backward compat), even though unused."""
159- mock_config .INFLUX_URL = "http://localhost:8086"
160- mock_config .INFLUX_TOKEN = "test-token"
161- mock_config .INFLUX_DB = "test-db"
134+ _make_wide_mock_config (mock_config )
162135
163136 mock_table = MagicMock ()
164137 mock_table .num_rows = 0
165138 mock_client = MagicMock ()
166139 mock_client .query .return_value = mock_table
167- mock_client .close = MagicMock ()
168140 mock_client_class .return_value = mock_client
169141
170- old_client = MagicMock ()
171142 result = discover_sensors (
172143 start_time = datetime (2025 , 1 , 1 ),
173144 end_time = datetime (2025 , 1 , 2 ),
174- client = old_client ,
145+ client = MagicMock () ,
175146 show_progress = False ,
176147 )
177148
178149 self .assertEqual (result , [])
179150
180151
152+ class TestDiscoverSensorsNarrow (unittest .TestCase ):
153+ """Tests for discover_sensors with narrow schema (legacy EAV, deprecated)."""
154+
155+ def _make_narrow_config (self , mock_config ):
156+ mock_config .INFLUX_URL = "http://localhost:8086"
157+ mock_config .INFLUX_TOKEN = "test-token"
158+ mock_config .INFLUX_SCHEMA = "iox"
159+ mock_config .INFLUX_TABLE = "WFR26"
160+ mock_config .INFLUX_DB = "WFR26"
161+
162+ @patch ('slicks.discovery.get_influx_client' )
163+ @patch ('slicks.discovery.config' )
164+ def test_emits_deprecation_warning (self , mock_config , mock_client_class ):
165+ """schema='narrow' should emit a DeprecationWarning."""
166+ self ._make_narrow_config (mock_config )
167+
168+ mock_table = MagicMock ()
169+ mock_table .num_rows = 0
170+ mock_client = MagicMock ()
171+ mock_client .query .return_value = mock_table
172+ mock_client_class .return_value = mock_client
173+
174+ with warnings .catch_warnings (record = True ) as caught :
175+ warnings .simplefilter ("always" )
176+ discover_sensors (
177+ start_time = datetime (2025 , 1 , 1 ),
178+ end_time = datetime (2025 , 1 , 2 ),
179+ schema = "narrow" ,
180+ show_progress = False ,
181+ )
182+
183+ deprecation_warnings = [w for w in caught if issubclass (w .category , DeprecationWarning )]
184+ self .assertEqual (len (deprecation_warnings ), 1 )
185+ self .assertIn ("narrow" , str (deprecation_warnings [0 ].message ))
186+ self .assertIn ("wide" , str (deprecation_warnings [0 ].message ))
187+
188+ @patch ('slicks.discovery.get_influx_client' )
189+ @patch ('slicks.discovery.config' )
190+ def test_raises_on_permanent_error (self , mock_config , mock_client_class ):
191+ """Should raise RuntimeError on auth failure."""
192+ self ._make_narrow_config (mock_config )
193+
194+ mock_client = MagicMock ()
195+ mock_client .query .side_effect = Exception ("Unauthorized: invalid token" )
196+ mock_client_class .return_value = mock_client
197+
198+ with warnings .catch_warnings ():
199+ warnings .simplefilter ("ignore" , DeprecationWarning )
200+ with self .assertRaises (RuntimeError ):
201+ discover_sensors (
202+ start_time = datetime (2025 , 1 , 1 ),
203+ end_time = datetime (2025 , 1 , 2 ),
204+ schema = "narrow" ,
205+ show_progress = False ,
206+ )
207+
208+ @patch ('slicks.discovery.get_influx_client' )
209+ @patch ('slicks.discovery.config' )
210+ def test_default_chunk_size_is_7_days (self , mock_config , mock_client_class ):
211+ """Default chunk_size_days=7 should produce 2 chunks for 10-day range."""
212+ self ._make_narrow_config (mock_config )
213+
214+ mock_table = MagicMock ()
215+ mock_table .num_rows = 0
216+ mock_client = MagicMock ()
217+ mock_client .query .return_value = mock_table
218+ mock_client_class .return_value = mock_client
219+
220+ with warnings .catch_warnings ():
221+ warnings .simplefilter ("ignore" , DeprecationWarning )
222+ discover_sensors (
223+ start_time = datetime (2025 , 1 , 1 ),
224+ end_time = datetime (2025 , 1 , 11 ),
225+ schema = "narrow" ,
226+ show_progress = False ,
227+ )
228+
229+ # 10-day range with 7-day chunks = 2 chunks, each triggers at least 1 query
230+ self .assertGreaterEqual (mock_client .query .call_count , 2 )
231+
232+
181233if __name__ == '__main__' :
182234 unittest .main ()
0 commit comments