Skip to content

Commit a21260a

Browse files
test: tighten assertions to kill display, timer, error, and edge-case mutants
- Use exact separator width checks (== instead of in) for all display functions - Add error byte isolation tests to kill bitwise OR→AND mutants - Verify timer off-at time is in the future (kills +→− and format mutants) - Add exact Yes/No and Active Faults line checks - Verify main() passes args correctly to async_main and asyncio.run - Add _expand_flame boundary tests for zero-weight and remaining_weight edge cases - Use exact rounding assertion for _convert_to_celsius - Verify _log_response preserves body preview with += (kills +=→= mutant) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b8f920a commit a21260a

5 files changed

Lines changed: 127 additions & 17 deletions

File tree

tests/test_b2c_login.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ def test_body_logged(self, caplog):
288288
resp = self._make_resp()
289289
with caplog.at_level(logging.DEBUG, "flameconnect"):
290290
_log_response(resp, "Hello body")
291-
assert "body" in caplog.text.lower()
292-
assert "Hello body" in caplog.text
291+
# Exact format prefix (kills "<<< body: %s" → "XX<<< body: %sXX")
292+
assert "<<< body: Hello body" in caplog.text
293293

294294
def test_long_body_truncated(self, caplog):
295295
resp = self._make_resp()
@@ -298,6 +298,8 @@ def test_long_body_truncated(self, caplog):
298298
_log_response(resp, long_body)
299299
assert "bytes total" in caplog.text
300300
assert "3000" in caplog.text
301+
# Body preview includes first 2000 chars (kills += → = mutant)
302+
assert "x" * 2000 in caplog.text
301303

302304
def test_body_exactly_2000_no_truncation(self, caplog):
303305
resp = self._make_resp()

tests/test_cli_commands.py

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def test_standby_no_temp_unit(self, capsys):
211211
out = capsys.readouterr().out
212212
lines = out.strip().split("\n")
213213
assert lines[0].strip() == "[321] Mode"
214-
assert "─" * 40 in lines[1]
214+
assert lines[1] == " " + "─" * 40
215215
assert "Mode: Standby" in out
216216
assert "Target Temp: 20.0\u00b0" in out
217217
# No unit suffix when temp_unit is None (empty string)
@@ -253,7 +253,7 @@ def test_all_fields_displayed(self, capsys):
253253
out = capsys.readouterr().out
254254
lines = out.strip().split("\n")
255255
assert lines[0].strip() == "[322] Flame Effect"
256-
assert "─" * 40 in lines[1]
256+
assert lines[1] == " " + "─" * 40
257257
assert " Flame: On" in out
258258
assert " Flame Speed: 4 / 5" in out
259259
assert " Brightness: Low" in out
@@ -285,10 +285,11 @@ def test_heat_on_no_temp_unit(self, capsys):
285285
out = capsys.readouterr().out
286286
lines = out.strip().split("\n")
287287
assert lines[0].strip() == "[323] Heat Settings"
288-
assert "─" * 40 in lines[1]
288+
assert lines[1] == " " + "─" * 40
289289
assert " Heat: On" in out
290290
assert " Heat Mode: Boost" in out
291-
assert " Setpoint Temp: 25.0\u00b0" in out
291+
# No unit suffix when temp_unit is None (kills "" → "XXXX" mutant)
292+
assert " Setpoint Temp: 25.0\u00b0\n" in out
292293
assert " Boost Duration: 15" in out
293294

294295
def test_heat_with_celsius(self, capsys):
@@ -321,7 +322,7 @@ def test_enabled(self, capsys):
321322
out = capsys.readouterr().out
322323
lines = out.strip().split("\n")
323324
assert lines[0].strip() == "[325] Heat Mode"
324-
assert "─" * 40 in lines[1]
325+
assert lines[1] == " " + "─" * 40
325326
assert " Heat Control: Enabled" in out
326327

327328
def test_software_disabled(self, capsys):
@@ -346,18 +347,35 @@ def test_timer_disabled(self, capsys):
346347
out = capsys.readouterr().out
347348
lines = out.strip().split("\n")
348349
assert lines[0].strip() == "[326] Timer Mode"
349-
assert "─" * 40 in lines[1]
350+
assert lines[1] == " " + "─" * 40
350351
assert " Timer: Disabled" in out
351352
assert " Duration: 0 min (0h 0m)" in out
352353
assert "Off at:" not in out
353354

354355
def test_timer_enabled_with_duration(self, capsys):
356+
from datetime import datetime, timedelta
357+
355358
param = TimerParam(timer_status=TimerStatus.ENABLED, duration=90)
359+
before = datetime.now()
356360
_display_timer(param)
361+
after = datetime.now()
357362
out = capsys.readouterr().out
358363
assert " Timer: Enabled" in out
359364
assert " Duration: 90 min (1h 30m)" in out
360365
assert " Off at:" in out
366+
# Verify the off-at time is in the future (kills + → − mutant)
367+
off_line = [line for line in out.split("\n") if "Off at:" in line][0]
368+
off_time_str = off_line.strip().split("Off at:")[1].strip()
369+
# Must be HH:MM format (kills strftime case mutation)
370+
assert len(off_time_str) == 5
371+
assert off_time_str[2] == ":"
372+
hour, minute = int(off_time_str[:2]), int(off_time_str[3:])
373+
assert 0 <= hour <= 23
374+
assert 0 <= minute <= 59
375+
# Verify time is approximately now + 90 min (kills + → − and // 60 → // 61)
376+
expected_min = (before + timedelta(minutes=90)).strftime("%H:%M")
377+
expected_max = (after + timedelta(minutes=90)).strftime("%H:%M")
378+
assert off_time_str in (expected_min, expected_max)
361379

362380
def test_timer_enabled_zero_duration(self, capsys):
363381
param = TimerParam(timer_status=TimerStatus.ENABLED, duration=0)
@@ -394,7 +412,7 @@ def test_version_display(self, capsys):
394412
out = capsys.readouterr().out
395413
lines = out.strip().split("\n")
396414
assert lines[0].strip() == "[327] Software Version"
397-
assert "─" * 40 in lines[1]
415+
assert lines[1] == " " + "─" * 40
398416
assert " UI Version: 1.2.3" in out
399417
assert " Control Version: 4.5.6" in out
400418
assert " Relay Version: 7.8.9" in out
@@ -409,7 +427,7 @@ def test_no_errors(self, capsys):
409427
out = capsys.readouterr().out
410428
lines = out.strip().split("\n")
411429
assert lines[0].strip() == "[329] Error"
412-
assert "─" * 40 in lines[1]
430+
assert lines[1] == " " + "─" * 40
413431
assert " Error Byte 1: 0x00 (00000000)" in out
414432
assert " Active Faults: None" in out
415433

@@ -432,6 +450,34 @@ def test_error_bytes_formatted(self, capsys):
432450
assert " Error Byte 4: 0x08 (00001000)" in out
433451
assert " Active Faults: Yes" in out
434452

453+
def test_error_only_byte3(self, capsys):
454+
"""Error in byte3 only: kills byte2|byte3 → byte2&byte3 mutant."""
455+
param = ErrorParam(error_byte1=0, error_byte2=0, error_byte3=1, error_byte4=0)
456+
_display_error(param)
457+
out = capsys.readouterr().out
458+
assert " Active Faults: Yes" in out
459+
460+
def test_error_only_byte4(self, capsys):
461+
"""Error in byte4 only: kills byte3|byte4 → byte3&byte4 mutant."""
462+
param = ErrorParam(error_byte1=0, error_byte2=0, error_byte3=0, error_byte4=1)
463+
_display_error(param)
464+
out = capsys.readouterr().out
465+
assert " Active Faults: Yes" in out
466+
467+
def test_active_faults_exact_yes(self, capsys):
468+
"""Exact match for Active Faults line (kills XX prefix/suffix)."""
469+
param = ErrorParam(error_byte1=1, error_byte2=0, error_byte3=0, error_byte4=0)
470+
_display_error(param)
471+
out = capsys.readouterr().out
472+
assert " Active Faults: Yes\n" in out
473+
474+
def test_active_faults_exact_none(self, capsys):
475+
"""Exact match for Active Faults line (kills XX prefix/suffix)."""
476+
param = ErrorParam(error_byte1=0, error_byte2=0, error_byte3=0, error_byte4=0)
477+
_display_error(param)
478+
out = capsys.readouterr().out
479+
assert " Active Faults: None\n" in out
480+
435481

436482
class TestDisplayTempUnit:
437483
"""Tests for _display_temp_unit()."""
@@ -442,7 +488,7 @@ def test_celsius(self, capsys):
442488
out = capsys.readouterr().out
443489
lines = out.strip().split("\n")
444490
assert lines[0].strip() == "[236] Temperature Unit"
445-
assert "─" * 40 in lines[1]
491+
assert lines[1] == " " + "─" * 40
446492
assert " Unit: Celsius" in out
447493

448494
def test_fahrenheit(self, capsys):
@@ -461,7 +507,7 @@ def test_display(self, capsys):
461507
out = capsys.readouterr().out
462508
lines = out.strip().split("\n")
463509
assert lines[0].strip() == "[369] Sound"
464-
assert "─" * 40 in lines[1]
510+
assert lines[1] == " " + "─" * 40
465511
assert " Volume: 128 / 255" in out
466512
assert " Sound File: 3" in out
467513

@@ -479,7 +525,7 @@ def test_on(self, capsys):
479525
out = capsys.readouterr().out
480526
lines = out.strip().split("\n")
481527
assert lines[0].strip() == "[370] Log Effect"
482-
assert "─" * 40 in lines[1]
528+
assert lines[1] == " " + "─" * 40
483529
assert " Log Effect: On" in out
484530
assert " Colors: RGBW(1, 0, 255, 128)" in out
485531
assert " Pattern: 5" in out
@@ -693,7 +739,10 @@ class TestDisplayFeatures:
693739
def test_all_false(self, capsys):
694740
_display_features(FireFeatures())
695741
out = capsys.readouterr().out
696-
assert "Supported Features" in out
742+
# Exact header (kills XX prefix/suffix on "\n Supported Features")
743+
assert "\n Supported Features\n" in out
744+
# Exact separator (kills '─' → 'XX─XX' and * 40 → * 41)
745+
assert " " + "─" * 40 + "\n" in out
697746
# All should show No
698747
assert "Yes" not in out
699748
assert out.count("No") == 24
@@ -705,6 +754,17 @@ def test_some_true(self, capsys):
705754
assert out.count("Yes") == 3
706755
assert out.count("No") == 21
707756

757+
def test_exact_yes_no_values(self, capsys):
758+
"""Exact Yes/No values (kills 'Yes' → 'XXYesXX', 'No' → 'XXNoXX')."""
759+
features = FireFeatures(sound=True)
760+
_display_features(features)
761+
out = capsys.readouterr().out
762+
lines = out.strip().split("\n")
763+
# Each feature line: " Label: Value"
764+
for line in lines[2:]: # skip header and separator
765+
value = line.rsplit(None, 1)[-1]
766+
assert value in ("Yes", "No"), f"Unexpected value: {value!r}"
767+
708768
def test_all_labels_present(self, capsys):
709769
_display_features(FireFeatures())
710770
out = capsys.readouterr().out
@@ -1442,10 +1502,11 @@ class TestMain:
14421502
"""Tests for the synchronous main() entry point."""
14431503

14441504
def test_main_calls_async_main(self):
1505+
mock_async_main = MagicMock()
14451506
with (
14461507
patch("flameconnect.cli.build_parser") as mock_parser_fn,
14471508
patch("flameconnect.cli.asyncio") as mock_asyncio,
1448-
patch("flameconnect.cli.async_main", new=MagicMock()),
1509+
patch("flameconnect.cli.async_main", new=mock_async_main),
14491510
):
14501511
mock_parser = MagicMock()
14511512
mock_args = argparse.Namespace(command="list", verbose=False)
@@ -1454,7 +1515,10 @@ def test_main_calls_async_main(self):
14541515

14551516
main()
14561517

1457-
mock_asyncio.run.assert_called_once()
1518+
# Verify async_main is called with args (kills → None mutant)
1519+
mock_async_main.assert_called_once_with(mock_args)
1520+
# Verify asyncio.run receives the coroutine (kills → None mutant)
1521+
mock_asyncio.run.assert_called_once_with(mock_async_main.return_value)
14581522

14591523
def test_main_verbose_logging(self):
14601524
with (

tests/test_fireplace_visual.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,41 @@ def test_gap_spaces_not_other_chars(self):
368368
gap = plain[1:-1]
369369
assert gap == " " * len(gap)
370370

371+
def test_all_zero_weights(self):
372+
"""All atoms with weight=0: gap uses `or 1` fallback (kills or 1 → or 2)."""
373+
atoms = [("A", 0), ("B", 0)]
374+
result = _expand_flame(atoms, 6, "red")
375+
# total_weight = 0, fallback to 1; gap_w is 0 for both so no gaps
376+
assert result.plain == "AB"
377+
378+
def test_gap_w_zero_skips_spacing(self):
379+
"""Atom with gap_w=0 doesn't enter spacing branch (kills > 0 → >= 0)."""
380+
atoms = [("A", 0), ("B", 1), ("C", 0)]
381+
result = _expand_flame(atoms, 7, "red")
382+
plain = result.plain
383+
# A has weight 0 → no gap after A
384+
# B has weight 1 → gets all 4 remaining gap
385+
assert plain == "A" + "B" + " " * 4 + "C"
386+
387+
def test_remaining_weight_reaches_zero(self):
388+
"""After consuming all weight, remaining atoms get no gap (kills > 0 → >= 0)."""
389+
# After first atom: remaining_weight = total_weight - gap_w = 1 - 1 = 0
390+
# Second atom has gap_w=0 so it won't enter the branch anyway,
391+
# but if it had gap_w > 0, remaining_weight=0 should prevent spacing.
392+
atoms = [("A", 1), ("B", 0)]
393+
result = _expand_flame(atoms, 10, "red")
394+
plain = result.plain
395+
# A gets all 8 gap, B gets none
396+
assert plain == "A" + " " * 8 + "B"
397+
398+
def test_and_vs_or_gap_w_zero_remaining_positive(self):
399+
"""With gap_w=0 but remaining_weight>0, no gap (kills and → or)."""
400+
atoms = [("X", 0), ("Y", 2), ("Z", 0)]
401+
result = _expand_flame(atoms, 9, "red")
402+
plain = result.plain
403+
# X: gap_w=0 → skip; Y: gap_w=2, remaining_weight=2, 6*2//2=6; Z: weight=0
404+
assert plain == "X" + "Y" + " " * 6 + "Z"
405+
371406

372407
# ---------------------------------------------------------------------------
373408
# _build_fire_art – heat rows

tests/test_tui_screens.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,8 @@ def test_convert_to_celsius(self):
755755
from flameconnect.tui.temperature_screen import _convert_to_celsius
756756

757757
result = _convert_to_celsius(72.0)
758-
assert abs(result - 22.2) < 0.1
758+
# Exact: round((72-32)*5/9, 1) = 22.2 (kills round(_, 1) → round(_, 2))
759+
assert result == 22.2
759760

760761
def test_convert_to_celsius_32(self):
761762
from flameconnect.tui.temperature_screen import _convert_to_celsius

tests/test_widgets_format.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,22 @@ def test_disabled_timer(self):
461461
assert result[0][2] == "toggle_timer"
462462

463463
def test_enabled_timer_with_duration(self):
464+
from datetime import datetime, timedelta
465+
464466
param = TimerParam(timer_status=TimerStatus.ENABLED, duration=30)
467+
before = datetime.now()
465468
result = _format_timer(param)
469+
after = datetime.now()
466470
value = result[0][1]
467471
assert value.startswith("Enabled Duration: 30min Off at ")
468472
# HH:MM format
469473
off_at = value.split("Off at ")[1]
470474
assert len(off_at) == 5 # HH:MM
471475
assert off_at[2] == ":"
476+
# Verify time is now + 30min (kills + → − mutant)
477+
expected_min = (before + timedelta(minutes=30)).strftime("%H:%M")
478+
expected_max = (after + timedelta(minutes=30)).strftime("%H:%M")
479+
assert off_at in (expected_min, expected_max)
472480

473481
def test_enabled_timer_zero_duration(self):
474482
"""Enabled timer with 0 duration should not show off-at time."""

0 commit comments

Comments
 (0)