Skip to content

Commit ae9bf43

Browse files
committed
implement hypothesis tests for siggen
1 parent 5e32a4d commit ae9bf43

1 file changed

Lines changed: 53 additions & 58 deletions

File tree

tests/test_siggen.py

Lines changed: 53 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010

1111
import numpy as np
1212
import numpy.testing as npt
13+
from hypothesis import assume, given
14+
from hypothesis import strategies as st
1315

1416
import sigmf
15-
from sigmf import SigMFFile
1617
from sigmf.error import SigMFGeneratorError
1718
from sigmf.siggen import SigMFGenerator
1819

@@ -212,20 +213,21 @@ def test_data_buffer_creation(self):
212213
# verify data is complex64
213214
self.assertEqual(samples_0.dtype, np.complex64)
214215

215-
def test_with_different_amplitudes(self):
216-
"""test amplitude parameter"""
217-
amp_low = 0.5
218-
amp_high = 1.5
219-
216+
@given(
217+
amp_low=st.floats(min_value=0.1, max_value=0.9, allow_nan=False, allow_infinity=False),
218+
amp_high=st.floats(min_value=1.1, max_value=3.0, allow_nan=False, allow_infinity=False),
219+
)
220+
def test_amplitude_power_ratio(self, amp_low, amp_high):
221+
"""test that power scales with amplitude squared for any pair of amplitudes"""
220222
signal_low = SigMFGenerator(self.seed).amplitude(amp_low).generate()
221223
signal_high = SigMFGenerator(self.seed).amplitude(amp_high).generate()
222224

223225
power_low = np.mean(np.abs(signal_low.read_samples()) ** 2)
224226
power_high = np.mean(np.abs(signal_high.read_samples()) ** 2)
225227

226-
expected_power_ratio = (amp_high / amp_low) ** 2
227-
actual_power_ratio = power_high / power_low
228-
self.assertAlmostEqual(actual_power_ratio, expected_power_ratio, places=1)
228+
expected_ratio = (amp_high / amp_low) ** 2
229+
actual_ratio = power_high / power_low
230+
npt.assert_almost_equal(actual_ratio, expected_ratio, decimal=1)
229231

230232
def test_automatic_annotations(self):
231233
"""test that appropriate annotations are automatically created"""
@@ -270,30 +272,22 @@ def test_automatic_annotations(self):
270272
offset_annotation = next(ann for ann in annotations if "freq offset" in ann.get(sigmf.LABEL_KEY, ""))
271273
self.assertIn("+200.0 Hz", offset_annotation[sigmf.LABEL_KEY])
272274

273-
def test_sweep_annotations(self):
274-
"""test sweep annotations have correct frequency bounds including negative"""
275-
signal = SigMFGenerator().sweep(-2500, 2500).sample_rate(22050).generate()
276-
277-
annotations = signal.get_annotations()
278-
self.assertEqual(len(annotations), 1) # just main sweep annotation
275+
@given(
276+
start_freq_hz=st.integers(min_value=-20000, max_value=20000),
277+
end_freq_hz=st.integers(min_value=-20000, max_value=20000),
278+
)
279+
def test_sweep_annotation_bounds(self, start_freq_hz, end_freq_hz):
280+
"""test sweep annotation bounds are always (min, max) regardless of direction, and label preserves original order"""
281+
assume(start_freq_hz != end_freq_hz)
282+
signal = SigMFGenerator().sweep(start_freq_hz, end_freq_hz).sample_rate(48000).generate()
283+
sweep_ann = signal.get_annotations()[0]
279284

280-
sweep_annotation = annotations[0]
281-
self.assertEqual(sweep_annotation[sigmf.FREQ_LOWER_EDGE_KEY], -2500.0)
282-
self.assertEqual(sweep_annotation[sigmf.FREQ_UPPER_EDGE_KEY], 2500.0)
283-
self.assertIn("sweep from -2500 to 2500 Hz", sweep_annotation[sigmf.LABEL_KEY])
285+
# bounds must be min/max regardless of sweep direction
286+
self.assertEqual(sweep_ann[sigmf.FREQ_LOWER_EDGE_KEY], float(min(start_freq_hz, end_freq_hz)))
287+
self.assertEqual(sweep_ann[sigmf.FREQ_UPPER_EDGE_KEY], float(max(start_freq_hz, end_freq_hz)))
284288

285-
def test_reverse_sweep_annotations(self):
286-
"""test reverse sweep crossing DC has correct bounds"""
287-
signal = SigMFGenerator().sweep(3000, -800).sample_rate(48000).generate()
288-
289-
annotations = signal.get_annotations()
290-
sweep_annotation = annotations[0]
291-
292-
# frequency bounds should be min/max regardless of sweep direction
293-
self.assertEqual(sweep_annotation[sigmf.FREQ_LOWER_EDGE_KEY], -800.0)
294-
self.assertEqual(sweep_annotation[sigmf.FREQ_UPPER_EDGE_KEY], 3000.0)
295-
# but label should show original order
296-
self.assertIn("sweep from 3000 to -800 Hz", sweep_annotation[sigmf.LABEL_KEY])
289+
# label must preserve the original start-to-end order
290+
self.assertIn(f"sweep from {start_freq_hz} to {end_freq_hz} Hz", sweep_ann[sigmf.LABEL_KEY])
297291

298292
def test_minimal_annotations(self):
299293
"""test that simple signals get minimal but complete annotations"""
@@ -340,38 +334,39 @@ def test_phase_offset(self):
340334
class TestEdgeCases(unittest.TestCase):
341335
"""Test edge cases and error conditions."""
342336

343-
def test_zero_duration(self):
344-
"""test zero duration raises error"""
345-
with self.assertRaises(SigMFGeneratorError):
346-
SigMFGenerator().duration(0).generate()
347-
348-
def test_negative_duration(self):
349-
"""test negative duration raises error"""
350-
with self.assertRaises(SigMFGeneratorError):
351-
SigMFGenerator().duration(-1.0).generate()
352-
353-
def test_negative_sample_rate(self):
354-
"""test negative sample rate raises error"""
337+
@given(st.floats(max_value=0.0, allow_nan=False, allow_infinity=False))
338+
def test_nonpositive_duration_raises(self, duration_s):
339+
"""test that any non-positive duration raises SigMFGeneratorError"""
355340
with self.assertRaises(SigMFGeneratorError):
356-
SigMFGenerator().sample_rate(-8000).generate()
341+
SigMFGenerator().duration(duration_s).generate()
357342

358-
def test_tone_nyquist_validation(self):
359-
"""test tone frequency exceeding nyquist raises error"""
360-
with self.assertRaises(SigMFGeneratorError):
361-
SigMFGenerator().tone(5000).sample_rate(8000).generate()
362-
with self.assertRaises(SigMFGeneratorError):
363-
SigMFGenerator().tone(-5000).sample_rate(8000).generate()
364-
365-
def test_sweep_nyquist_validation(self):
366-
"""test sweep frequencies exceeding nyquist raise error"""
367-
with self.assertRaises(SigMFGeneratorError):
368-
SigMFGenerator().sweep(1000, 5000).sample_rate(8000).generate()
343+
@given(st.integers(max_value=0))
344+
def test_nonpositive_sample_rate_raises(self, samp_rate_hz):
345+
"""test that any non-positive sample rate raises SigMFGeneratorError"""
369346
with self.assertRaises(SigMFGeneratorError):
370-
SigMFGenerator().sweep(5000, 1000).sample_rate(8000).generate()
347+
SigMFGenerator().sample_rate(samp_rate_hz).generate()
348+
349+
@given(
350+
samp_rate_hz=st.integers(min_value=100, max_value=200000),
351+
freq_hz=st.floats(allow_nan=False, allow_infinity=False),
352+
)
353+
def test_tone_nyquist_raises(self, samp_rate_hz, freq_hz):
354+
"""test that any tone frequency exceeding nyquist raises SigMFGeneratorError"""
355+
assume(abs(freq_hz) > samp_rate_hz / 2)
371356
with self.assertRaises(SigMFGeneratorError):
372-
SigMFGenerator().sweep(1000, -5000).sample_rate(8000).generate()
357+
SigMFGenerator().tone(freq_hz).sample_rate(samp_rate_hz).generate()
358+
359+
@given(
360+
samp_rate_hz=st.integers(min_value=100, max_value=200000),
361+
start_freq_hz=st.floats(allow_nan=False, allow_infinity=False),
362+
end_freq_hz=st.floats(allow_nan=False, allow_infinity=False),
363+
)
364+
def test_sweep_nyquist_raises(self, samp_rate_hz, start_freq_hz, end_freq_hz):
365+
"""test that any sweep with at least one frequency exceeding nyquist raises SigMFGeneratorError"""
366+
nyquist_hz = samp_rate_hz / 2
367+
assume(abs(start_freq_hz) > nyquist_hz or abs(end_freq_hz) > nyquist_hz)
373368
with self.assertRaises(SigMFGeneratorError):
374-
SigMFGenerator().sweep(-5000, 1000).sample_rate(8000).generate()
369+
SigMFGenerator().sweep(start_freq_hz, end_freq_hz).sample_rate(samp_rate_hz).generate()
375370

376371
def test_sweep_same_start_end_frequency(self):
377372
"""test sweep with same start and end frequency"""

0 commit comments

Comments
 (0)