@@ -355,4 +355,150 @@ def test_complex_list_hierarchy_overflow(self):
355355 # Test with reference level on list hierarchy
356356 result = meter .get_musical_time (1.5 , reference_level = 0 )
357357 assert result is not False
358- assert len (result .hierarchical_position ) == 1
358+ assert len (result .hierarchical_position ) == 1
359+
360+ def test_reference_level_zero_indexerror_reproduction (self ):
361+ """Test to reproduce IndexError with reference_level=0 (Issue #26)."""
362+ # Try different meter configurations that might trigger the error
363+ test_configs = [
364+ ([4 , 4 , 2 ], 120 ),
365+ ([2 , 3 , 4 ], 60 ),
366+ ([3 , 2 ], 240 ),
367+ ([8 ], 120 ),
368+ ([2 , 2 , 2 , 2 ], 180 )
369+ ]
370+
371+ for hierarchy , tempo in test_configs :
372+ meter = Meter (hierarchy = hierarchy , tempo = tempo , start_time = 0 )
373+
374+ # Test various time points within the meter
375+ cycle_duration = meter .cycle_dur
376+ test_times = [
377+ 0.1 , # Near start
378+ cycle_duration * 0.25 , # Quarter way through
379+ cycle_duration * 0.5 , # Half way
380+ cycle_duration * 0.75 , # Three quarters
381+ cycle_duration * 0.99 , # Near end
382+ ]
383+
384+ for time_point in test_times :
385+ try :
386+ result = meter .get_musical_time (time_point , reference_level = 0 )
387+ if result is not False : # Only check if within meter bounds
388+ assert len (result .hierarchical_position ) == 1 , f"Should have 1 position for reference_level=0 with hierarchy { hierarchy } "
389+ assert result .hierarchical_position [0 ] >= 0 , "Position should be non-negative"
390+ except IndexError as e :
391+ pytest .fail (f"IndexError raised with hierarchy { hierarchy } , tempo { tempo } , time { time_point } , reference_level=0: { e } " )
392+ except Exception as e :
393+ # Let other exceptions bubble up with context
394+ pytest .fail (f"Unexpected error with hierarchy { hierarchy } , tempo { tempo } , time { time_point } : { e } " )
395+
396+ # Test edge case: reference_level=0 with positions that might cause overflow
397+ meter = Meter (hierarchy = [2 , 2 ], tempo = 60 , start_time = 0 )
398+ try :
399+ # Test at exact beat boundaries which might cause index issues
400+ result = meter .get_musical_time (1.0 , reference_level = 0 ) # Exactly at beat 1
401+ if result is not False :
402+ assert len (result .hierarchical_position ) == 1
403+ except IndexError as e :
404+ pytest .fail (f"IndexError at beat boundary with reference_level=0: { e } " )
405+
406+ # Test with multi-cycle meter - this might trigger the error
407+ meter = Meter (hierarchy = [4 , 4 , 2 ], tempo = 120 , start_time = 0 , repetitions = 2 )
408+ try :
409+ # Test near the end of cycle or at various points
410+ test_times = [meter .cycle_dur - 0.01 , meter .cycle_dur + 0.01 , meter .cycle_dur * 1.5 ]
411+ for t in test_times :
412+ result = meter .get_musical_time (t , reference_level = 0 )
413+ if result is not False :
414+ assert len (result .hierarchical_position ) == 1
415+ except IndexError as e :
416+ pytest .fail (f"IndexError with multi-cycle meter and reference_level=0: { e } " )
417+
418+ # Test very specific timing that might trigger calculation edge case
419+ meter = Meter (hierarchy = [4 , 4 , 2 ], tempo = 120 , start_time = 0 )
420+ try :
421+ # Test at the end of each beat - this is where overflow might happen
422+ beat_duration = 60.0 / 120 # 0.5 seconds per beat at 120 BPM
423+ for beat in range (4 ): # Test each beat in the cycle
424+ time_at_end_of_beat = beat_duration * (beat + 1 ) - 0.001 # Just before next beat
425+ result = meter .get_musical_time (time_at_end_of_beat , reference_level = 0 )
426+ if result is not False :
427+ assert len (result .hierarchical_position ) == 1
428+ except IndexError as e :
429+ pytest .fail (f"IndexError at beat boundaries with reference_level=0: { e } " )
430+
431+ # Test the specific case where next_positions causes pulse index overflow
432+ # This happens when we're at the last beat of a cycle with reference_level=0
433+ meter = Meter (hierarchy = [4 , 2 ], tempo = 120 , start_time = 0 , repetitions = 1 )
434+ try :
435+ # Get close to the end of the last beat (beat 3, index 3 in hierarchy [4, 2])
436+ # With tempo 120, beat duration is 0.5 seconds
437+ # Total cycle duration should be 4 beats * 0.5 = 2.0 seconds
438+ # Let's test at beat 3.9 (just before beat 4, which would overflow)
439+ time_near_end = 3.9 * 0.5 # Should be 1.95 seconds
440+ result = meter .get_musical_time (time_near_end , reference_level = 0 )
441+ if result is not False :
442+ assert len (result .hierarchical_position ) == 1
443+ # This should trigger the duration calculation that tries to find the "next beat"
444+ # which would be beat 4 (index 4), causing overflow since hierarchy[0] = 4 (indices 0,1,2,3)
445+ except IndexError as e :
446+ pytest .fail (f"IndexError when calculating duration near end of cycle with reference_level=0: { e } " )
447+
448+ # Even more specific test - try to force the exact overflow scenario
449+ meter = Meter (hierarchy = [2 ], tempo = 60 , start_time = 0 , repetitions = 1 )
450+ try :
451+ # With hierarchy [2], we have beats 0 and 1
452+ # Test at beat 1 (the last beat) - this should cause next_position[0] = 2, which overflows
453+ beat_1_time = 1.0 * (60.0 / 60.0 ) * 0.9 # 90% through beat 1
454+ result = meter .get_musical_time (beat_1_time , reference_level = 0 )
455+ if result is not False :
456+ assert len (result .hierarchical_position ) == 1
457+ except IndexError as e :
458+ pytest .fail (f"IndexError with simple [2] hierarchy at last beat with reference_level=0: { e } " )
459+
460+ def test_reference_level_zero_bounds_checking (self ):
461+ """Test that bounds checking prevents IndexError when pulse index exceeds bounds."""
462+ # Test with a simple meter where we can predictably hit boundary conditions
463+ meter = Meter (hierarchy = [2 , 2 ], tempo = 60 , start_time = 0 , repetitions = 1 )
464+
465+ # Test at various points including near boundaries
466+ # The key is testing reference_level=0 which might try to calculate duration
467+ # by looking for the "next beat" which could exceed pulse array bounds
468+ test_times = []
469+ beat_duration = 60.0 / 60.0 # 1 second per beat at 60 BPM
470+
471+ # Add times throughout the meter, especially near beat boundaries
472+ for beat in range (2 ): # 2 beats in hierarchy [2, 2]
473+ for fraction in [0.1 , 0.5 , 0.9 , 0.99 ]:
474+ test_time = beat * beat_duration + fraction * beat_duration
475+ test_times .append (test_time )
476+
477+ # Test all time points with reference_level=0
478+ for time_point in test_times :
479+ result = meter .get_musical_time (time_point , reference_level = 0 )
480+ if result is not False :
481+ assert len (result .hierarchical_position ) == 1 , f"Should have 1 position at time { time_point } "
482+ assert isinstance (result .fractional_beat , float ), f"Should have valid fractional_beat at time { time_point } "
483+ assert 0.0 <= result .fractional_beat <= 1.0 , f"fractional_beat should be in [0,1] at time { time_point } "
484+
485+ # Test specifically at the boundary that might cause the original IndexError
486+ # When we're in the last beat and try to calculate duration
487+ last_beat_time = 1.8 # Near end of beat 1 (last beat) in a 2-beat cycle
488+ result = meter .get_musical_time (last_beat_time , reference_level = 0 )
489+ if result is not False :
490+ assert len (result .hierarchical_position ) == 1
491+ assert result .hierarchical_position [0 ] == 1 # Should be in beat 1 (second beat)
492+
493+ def test_defensive_bounds_in_calculate_level_start_time (self ):
494+ """Test that _calculate_level_start_time handles out-of-bounds indices gracefully."""
495+ meter = Meter (hierarchy = [3 ], tempo = 120 , start_time = 0 , repetitions = 1 )
496+
497+ # This should work without IndexError even if internal calculations go out of bounds
498+ # Test near the end of the cycle where "next beat" calculations might overflow
499+ near_end_time = meter .cycle_dur * 0.95
500+ result = meter .get_musical_time (near_end_time , reference_level = 0 )
501+
502+ if result is not False :
503+ assert len (result .hierarchical_position ) == 1
504+ # Should not crash and should give reasonable results
0 commit comments