44import tempfile
55from unittest .mock import MagicMock , patch
66
7- import numpy as np
87import pytest
98
109import birdnet_analyzer .config as cfg
@@ -32,9 +31,7 @@ def setup_test_environment():
3231 f .write (b"more dummy audio data" )
3332
3433 # Store original config values
35- original_config = {
36- attr : getattr (cfg , attr ) for attr in dir (cfg ) if not attr .startswith ("_" ) and not callable (getattr (cfg , attr ))
37- }
34+ original_config = {attr : getattr (cfg , attr ) for attr in dir (cfg ) if not attr .startswith ("_" ) and not callable (getattr (cfg , attr ))}
3835
3936 yield {
4037 "test_dir" : test_dir ,
@@ -88,9 +85,7 @@ def test_analyze_single_file(
8885@patch ("birdnet_analyzer.analyze.core._set_params" )
8986@patch ("multiprocessing.Pool" )
9087@patch ("birdnet_analyzer.analyze.utils.save_analysis_params" )
91- def test_analyze_directory_multiprocess (
92- mock_save_params : MagicMock , mock_pool , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment
93- ):
88+ def test_analyze_directory_multiprocess (mock_save_params : MagicMock , mock_pool , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment ):
9489 """Test analyzing multiple files with multiprocessing."""
9590 env = setup_test_environment
9691
@@ -180,6 +175,7 @@ def test_analyze_with_location_filtering(mock_analyze_file: MagicMock, mock_set_
180175
181176 # Verify parameter passing
182177 mock_set_params .assert_called_once ()
178+ mock_ensure_model .assert_called_once ()
183179 _ , kwargs = mock_set_params .call_args
184180 assert kwargs ["lat" ] == 42.5
185181 assert kwargs ["lon" ] == - 76.45
@@ -207,16 +203,15 @@ def test_analyze_with_custom_classifier(mock_analyze_file: MagicMock, mock_set_p
207203
208204 # Verify parameter passing
209205 mock_set_params .assert_called_once ()
206+ mock_ensure_model .assert_called_once ()
210207 _ , kwargs = mock_set_params .call_args
211208 assert kwargs ["custom_classifier" ] == custom_classifier
212209
213210
214211@patch ("birdnet_analyzer.utils.ensure_model_exists" )
215212@patch ("birdnet_analyzer.analyze.core._set_params" )
216213@patch ("birdnet_analyzer.analyze.utils.analyze_file" )
217- def test_analyze_with_multiple_result_types (
218- mock_analyze_file : MagicMock , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment
219- ):
214+ def test_analyze_with_multiple_result_types (mock_analyze_file : MagicMock , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment ):
220215 """Test analyzing with multiple output result types."""
221216 env = setup_test_environment
222217
@@ -229,16 +224,15 @@ def test_analyze_with_multiple_result_types(
229224
230225 # Verify parameter passing
231226 mock_set_params .assert_called_once ()
227+ mock_ensure_model .assert_called_once ()
232228 _ , kwargs = mock_set_params .call_args
233229 assert kwargs ["rtype" ] == ["table" , "csv" , "audacity" ]
234230
235231
236232@patch ("birdnet_analyzer.utils.ensure_model_exists" )
237233@patch ("birdnet_analyzer.analyze.core._set_params" )
238234@patch ("birdnet_analyzer.analyze.utils.analyze_file" )
239- def test_analyze_with_custom_species_list (
240- mock_analyze_file : MagicMock , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment
241- ):
235+ def test_analyze_with_custom_species_list (mock_analyze_file : MagicMock , mock_set_params : MagicMock , mock_ensure_model : MagicMock , setup_test_environment ):
242236 """Test analyzing with a custom species list."""
243237 env = setup_test_environment
244238
@@ -256,59 +250,126 @@ def test_analyze_with_custom_species_list(
256250
257251 # Verify parameter passing
258252 mock_set_params .assert_called_once ()
253+ mock_ensure_model .assert_called_once ()
259254 _ , kwargs = mock_set_params .call_args
260255 assert kwargs ["slist" ] == species_list
261256
262- def test_analyze_with_speed_up (setup_test_environment ):
263- """Test analyzing with speed up."""
257+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
258+ def test_analyze_with_negative_speed (setup_test_environment ):
259+ """Test analyzing with negative speed."""
264260 env = setup_test_environment
265261
266262 soundscape_path = "birdnet_analyzer/example/soundscape.wav"
267263
268264 assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
269265
270266 # Call function under test
271- analyze (soundscape_path , env ["output_dir" ], audio_speed = 5.0 , top_n = 1 , min_conf = 0 )
267+ with pytest .raises (ValueError , match = "Audio speed must be a positive value." ):
268+ analyze (soundscape_path , env ["output_dir" ], audio_speed = - 1.0 , top_n = 1 , min_conf = 0 )
272269
273- output_file = os .path .join (env ["output_dir" ], "soundscape.BirdNET.selection.table.txt" )
274- assert os .path .exists (output_file )
270+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
271+ def test_analyze_with_zero_speed (setup_test_environment ):
272+ """Test analyzing with zero speed."""
273+ env = setup_test_environment
275274
276- with open (output_file ) as f :
277- lines = f .readlines ()[1 :]
278- assert len (lines ) == 8 , "Number of predicted segments does not match"
275+ soundscape_path = "birdnet_analyzer/example/soundscape.wav"
279276
280- for index , line in enumerate (lines ):
281- parts = line .strip ().split ("\t " )
282- start = float (parts [3 ])
283- end = float (parts [4 ])
284- assert np .isclose (start , index * 15 ), "Start time does not match expected value"
285- assert np .isclose (end , (index + 1 ) * 15 ), "End time does not match expected value"
277+ assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
278+
279+ # Call function under test
280+ with pytest .raises (ValueError , match = "Audio speed must be a positive value." ):
281+ analyze (soundscape_path , env ["output_dir" ], audio_speed = 0.0 , top_n = 1 , min_conf = 0 )
282+
283+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
284+ def test_analyze_with_invalid_audio_speed (setup_test_environment ):
285+ """Test analyzing with invalid audio speed."""
286+ env = setup_test_environment
287+
288+ soundscape_path = "birdnet_analyzer/example/soundscape.wav"
289+
290+ assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
291+
292+ # Call function under test
293+ with pytest .raises (ValueError , match = "Audio speed must be a numeric value." ):
294+ analyze (soundscape_path , env ["output_dir" ], audio_speed = "fast" , top_n = 1 , min_conf = 0 )
295+
296+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
297+ def test_analyze_with_negative_overlap (setup_test_environment ):
298+ """Test analyzing with invalid overlap."""
299+ env = setup_test_environment
286300
301+ soundscape_path = "birdnet_analyzer/example/soundscape.wav"
302+
303+ assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
287304
288- def test_analyze_with_slow_down (setup_test_environment ):
305+ # Call function under test
306+ with pytest .raises (ValueError , match = "Overlap must be a non-negative value." ):
307+ analyze (soundscape_path , env ["output_dir" ], audio_speed = 1.0 , top_n = 1 , overlap = - 1 )
308+
309+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
310+ def test_analyze_with_invalid_overlap (setup_test_environment ):
311+ """Test analyzing with invalid overlap."""
312+ env = setup_test_environment
313+
314+ soundscape_path = "birdnet_analyzer/example/soundscape.wav"
315+
316+ assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
317+
318+ # Call function under test
319+ with pytest .raises (ValueError , match = "Overlap must be a numeric value." ):
320+ analyze (soundscape_path , env ["output_dir" ], audio_speed = 1.0 , top_n = 1 , overlap = "high" )
321+
322+ @patch ("birdnet_analyzer.utils.ensure_model_exists" )
323+ def test_analyze_with_too_high_overlap (setup_test_environment ):
324+ """Test analyzing with too high overlap."""
325+ env = setup_test_environment
326+
327+ soundscape_path = "birdnet_analyzer/example/soundscape.wav"
328+
329+ assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
330+
331+ # Call function under test
332+ with pytest .raises (ValueError , match = f"Overlap must be less than { cfg .SIG_LENGTH } seconds." ):
333+ analyze (soundscape_path , env ["output_dir" ], audio_speed = 1.0 , top_n = 1 , overlap = 3.0 )
334+
335+ @pytest .mark .parametrize (
336+ ("audio_speed" , "overlap" ),
337+ [(10 , 1 ), (5 , 2 ), (5 , 0 ), (0.1 , 1 ), (0.2 , 0 )],
338+ )
339+ def test_analyze_with_speed_up_and_overlap (setup_test_environment , audio_speed , overlap ):
289340 """Test analyzing with speed up."""
290341 env = setup_test_environment
291342
292343 soundscape_path = "birdnet_analyzer/example/soundscape.wav"
293344
294345 assert os .path .exists (soundscape_path ), "Soundscape file does not exist"
346+ file_length = 120
347+ step_size = round (3 * audio_speed - overlap * audio_speed , 1 )
348+ expected_start_timestamps = [e / 10 for e in range (0 , int (file_length * 10 ), int (step_size * 10 ))]
349+ expected_end_timestamps = [e / 10 for e in range (int (3 * audio_speed * 10 ), int (file_length ) * 10 + 1 , int (step_size * 10 ))]
350+
351+ while len (expected_end_timestamps ) < len (expected_start_timestamps ):
352+ if file_length - expected_start_timestamps [- 1 ] >= 1 * audio_speed :
353+ expected_end_timestamps .append (file_length )
354+ else :
355+ expected_start_timestamps .pop ()
295356
296357 # Call function under test
297- analyze (soundscape_path , env ["output_dir" ], audio_speed = 0.2 , top_n = 1 , min_conf = 0 )
358+ analyze (soundscape_path , env ["output_dir" ], audio_speed = audio_speed , top_n = 1 , overlap = overlap )
298359
299360 output_file = os .path .join (env ["output_dir" ], "soundscape.BirdNET.selection.table.txt" )
300361 assert os .path .exists (output_file )
301362
302363 with open (output_file ) as f :
303364 lines = f .readlines ()[1 :]
304- assert len (lines ) == 200 , "Number of predicted segments does not match"
305365
306- for index , line in enumerate ( lines ):
366+ for expected_start , expected_end , line in zip ( expected_start_timestamps , expected_end_timestamps , lines , strict = True ):
307367 parts = line .strip ().split ("\t " )
308- start = float (parts [3 ])
309- end = float (parts [4 ])
310- assert np .isclose (start , index * 0.6 ), "Start time does not match expected value"
311- assert np .isclose (end , (index + 1 ) * 0.6 ), "End time does not match expected value"
368+ actual_start = float (parts [3 ])
369+ actual_end = float (parts [4 ])
370+ assert float (actual_start ) == expected_start , "Start time does not match expected value"
371+ assert float (actual_end ) == expected_end , "End time does not match expected value"
372+
312373
313374@patch ("birdnet_analyzer.utils.ensure_model_exists" )
314375def test_analyze_with_additional_columns (mock_ensure_model , setup_test_environment ):
@@ -332,6 +393,7 @@ def test_analyze_with_additional_columns(mock_ensure_model, setup_test_environme
332393 rtype = ["csv" ],
333394 )
334395
396+ mock_ensure_model .assert_called_once ()
335397 output_file = os .path .join (env ["output_dir" ], "soundscape.BirdNET.results.csv" )
336398 assert os .path .exists (output_file )
337399
0 commit comments