1313# Last Update: July 11, 2026
1414##--------------------------------------------------------------------\
1515
16+ import logging
17+
1618import pytest
1719
1820from hackrfpy import HackRF , parse_freq , constants as C
@@ -50,27 +52,42 @@ def fake_run(argv, **k):
5052
5153
5254# ---- warn channel: safety warnings must fire regardless of verbose ----------
53- def test_snap_gain_warns_on_stderr_when_not_verbose (capsys ):
55+ # Warnings now go through logging ("hackrfpy" logger, WARNING level) instead of
56+ # a raw stderr write, so these assert on caplog. The stdout-purity invariant is
57+ # still checked with capsys: diagnostics must NEVER land on stdout.
58+ def test_snap_gain_warns_when_not_verbose (caplog ):
5459 h = HackRF (verbose = False )
55- snapped = h ._snap_gain ("lna" , 7 , C .LNA_GAIN ) # 7 -> 0, a silent 7 dB loss
60+ with caplog .at_level (logging .WARNING , logger = "hackrfpy" ):
61+ snapped = h ._snap_gain ("lna" , 7 , C .LNA_GAIN ) # 7 -> 0, a silent 7 dB loss
5662 assert snapped == 0
57- err = capsys . readouterr (). err
58- assert "lna" in err and "-> 0" in err # user was told, on stderr
63+ msgs = " \n " . join ( r . message for r in caplog . records )
64+ assert "lna" in msgs and "-> 0" in msgs # user was told, despite verbose=False
5965
6066
61- def test_sub_recommended_sample_rate_warns_not_silent (capsys ):
67+ def test_sub_recommended_sample_rate_warns_not_silent (caplog ):
6268 h = HackRF (verbose = False )
63- h ._check_hard_range ("sample_rate" , 4e6 , C .SR_MIN , C .SR_MAX , C .SR_WARN_BELOW )
64- assert "below the recommended" in capsys .readouterr ().err
69+ with caplog .at_level (logging .WARNING , logger = "hackrfpy" ):
70+ h ._check_hard_range ("sample_rate" , 4e6 , C .SR_MIN , C .SR_MAX , C .SR_WARN_BELOW )
71+ assert "below the recommended" in "\n " .join (r .message for r in caplog .records )
6572
6673
67- def test_forced_out_of_spec_warns_on_stderr ( capsys ):
74+ def test_forced_out_of_spec_warns_and_never_touches_stdout ( caplog , capsys ):
6875 h = HackRF ()
6976 h .allow_out_of_spec = True
70- h ._check_hard_range ("frequency" , 60e9 , C .FREQ_MIN_HZ , C .FREQ_MAX_HZ )
71- out , err = capsys .readouterr ()
72- assert "forced out-of-spec" in err
73- assert out == "" # never pollutes stdout
77+ with caplog .at_level (logging .WARNING , logger = "hackrfpy" ):
78+ h ._check_hard_range ("frequency" , 60e9 , C .FREQ_MIN_HZ , C .FREQ_MAX_HZ )
79+ assert "forced out-of-spec" in "\n " .join (r .message for r in caplog .records )
80+ assert capsys .readouterr ().out == "" # never pollutes stdout
81+
82+
83+ def test_warnings_are_warning_level (caplog ):
84+ # Level matters: a consumer filtering at WARNING must still receive safety
85+ # notices. Pinned so a future refactor can't quietly demote these to INFO.
86+ h = HackRF (verbose = False )
87+ with caplog .at_level (logging .WARNING , logger = "hackrfpy" ):
88+ h ._snap_gain ("lna" , 7 , C .LNA_GAIN )
89+ assert caplog .records
90+ assert all (r .levelno == logging .WARNING for r in caplog .records )
7491
7592
7693# ---- typed frequency parsing ------------------------------------------------
@@ -184,3 +201,40 @@ def test_transmit_print_cmd_skips_source_check(monkeypatch):
184201 h .transmit (433.92e6 , 8e6 , "/no/such/file.iq" , print_cmd = True )
185202 assert seen ["argv" ][0 ] == "transfer"
186203 assert "-t" in seen ["argv" ]
204+
205+
206+ # ---- logging contract: what a consumer is entitled to rely on ---------------
207+ def test_diagnostics_never_reach_stdout (capsys , caplog ):
208+ # The reason print_message moved off stdout: `hrf sweep -v > out.csv` used
209+ # to prepend "[*] mode: rx" INTO the CSV. stdout is data; stderr is chatter.
210+ h = HackRF (verbose = True )
211+ with caplog .at_level (logging .INFO , logger = "hackrfpy" ):
212+ h .print_message ("[*] progress chatter" )
213+ h .warn ("a safety warning" )
214+ assert capsys .readouterr ().out == ""
215+ assert len (caplog .records ) == 2
216+
217+
218+ def test_consumer_can_silence_the_library (caplog ):
219+ # Raising the level on the "hackrfpy" logger must suppress our records --
220+ # impossible back when these were bare print() calls.
221+ h = HackRF (verbose = False )
222+ logger = logging .getLogger ("hackrfpy" )
223+ with caplog .at_level (logging .CRITICAL , logger = "hackrfpy" ):
224+ logger .setLevel (logging .CRITICAL )
225+ try :
226+ h .warn ("should be suppressed" )
227+ finally :
228+ logger .setLevel (logging .NOTSET ) # restore; logger is global
229+ assert not [r for r in caplog .records if "suppressed" in r .message ]
230+
231+
232+ def test_verbose_gating_still_holds (caplog ):
233+ # verbose=False -> no INFO; verbose=True -> INFO flows.
234+ with caplog .at_level (logging .INFO , logger = "hackrfpy" ):
235+ quiet = HackRF (verbose = False )
236+ quiet .print_message ("[*] invisible" )
237+ assert not caplog .records
238+ quiet .set_verbose (True )
239+ quiet .print_message ("[*] now visible" )
240+ assert any ("now visible" in r .message for r in caplog .records )
0 commit comments