Skip to content

Commit 16d3ddf

Browse files
authored
[FIX] Increasing Tests Coverage Back to The Threshold (#101)
* Increasing Unit Tests coverage back to the required minimum percentage * Applying gemini suggestions: Using specific assertions and more
1 parent fec0511 commit 16d3ddf

2 files changed

Lines changed: 542 additions & 0 deletions

File tree

tests/test_run/test_log_stream_handler.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,241 @@ def test_uses_local_ip(self):
299299
with patch.object(h, "_get_local_ip", return_value="172.16.0.5"):
300300
url = h._get_log_viewer_url()
301301
assert "172.16.0.5" in url
302+
303+
304+
# ---------------------------------------------------------------------------
305+
# stop() — error path
306+
# ---------------------------------------------------------------------------
307+
308+
309+
@pytest.mark.unit
310+
class TestLogStreamHandlerStopError:
311+
def test_logs_error_when_http_server_stop_raises(self):
312+
h, mock_srv = _make_handler()
313+
h.is_running = True
314+
mock_srv.stop.side_effect = RuntimeError("stop failed")
315+
with patch("th_cli.test_run.log_stream_handler.logger") as mock_logger:
316+
h.stop()
317+
mock_logger.error.assert_called_once_with("Error stopping log stream handler: stop failed")
318+
319+
320+
# ---------------------------------------------------------------------------
321+
# init_tree()
322+
# ---------------------------------------------------------------------------
323+
324+
325+
@pytest.mark.unit
326+
class TestInitTree:
327+
def test_noop_when_not_running(self):
328+
h, _ = _make_handler()
329+
h.is_running = False
330+
with patch.object(h, "_broadcast") as mock_bcast:
331+
h.init_tree(MagicMock())
332+
mock_bcast.assert_not_called()
333+
334+
def test_broadcasts_tree_init_event(self):
335+
h, _ = _make_handler()
336+
h.is_running = True
337+
client_q = queue.Queue()
338+
h._clients.add(client_q)
339+
run = MagicMock()
340+
run.title = "My Run"
341+
run.test_suite_executions = []
342+
with patch("th_cli.test_run.log_stream_handler.logger"):
343+
h.init_tree(run)
344+
event = client_q.get_nowait()
345+
assert event["type"] == "tree_init"
346+
assert "data" in event
347+
348+
def test_updates_tree_state_in_place(self):
349+
h, _ = _make_handler()
350+
h.is_running = True
351+
run = MagicMock()
352+
run.title = "Run Title"
353+
run.test_suite_executions = []
354+
with patch("th_cli.test_run.log_stream_handler.logger"):
355+
h.init_tree(run)
356+
assert h.tree_state.get("title") == "Run Title"
357+
358+
def test_handles_build_exception_silently(self):
359+
h, _ = _make_handler()
360+
h.is_running = True
361+
with patch.object(h, "_build_tree", side_effect=RuntimeError("fail")):
362+
with patch("th_cli.test_run.log_stream_handler.logger") as mock_logger:
363+
h.init_tree(MagicMock())
364+
mock_logger.debug.assert_called_once_with("Error initialising tree: fail")
365+
366+
367+
# ---------------------------------------------------------------------------
368+
# update_tree_node()
369+
# ---------------------------------------------------------------------------
370+
371+
372+
@pytest.mark.unit
373+
class TestUpdateTreeNode:
374+
def test_noop_when_not_running(self):
375+
h, _ = _make_handler()
376+
h.is_running = False
377+
with patch.object(h, "_broadcast") as mock_bcast:
378+
h.update_tree_node(state="passed")
379+
mock_bcast.assert_not_called()
380+
381+
def test_run_level_update(self):
382+
h, _ = _make_handler()
383+
h.is_running = True
384+
h.tree_state = {"state": "pending", "suites": []}
385+
client_q = queue.Queue()
386+
h._clients.add(client_q)
387+
h.update_tree_node(state="executing")
388+
assert h.tree_state["state"] == "executing"
389+
event = client_q.get_nowait()
390+
assert event["node_type"] == "run"
391+
assert event["state"] == "executing"
392+
393+
def test_suite_level_update(self):
394+
h, _ = _make_handler()
395+
h.is_running = True
396+
h.tree_state = {"suites": [{"state": "pending", "cases": []}]}
397+
client_q = queue.Queue()
398+
h._clients.add(client_q)
399+
h.update_tree_node(state="passed", suite_idx=0)
400+
assert h.tree_state["suites"][0]["state"] == "passed"
401+
event = client_q.get_nowait()
402+
assert event["node_type"] == "suite"
403+
404+
def test_case_level_update(self):
405+
h, _ = _make_handler()
406+
h.is_running = True
407+
h.tree_state = {"suites": [{"cases": [{"state": "pending", "steps": []}]}]}
408+
client_q = queue.Queue()
409+
h._clients.add(client_q)
410+
h.update_tree_node(state="failed", suite_idx=0, case_idx=0)
411+
assert h.tree_state["suites"][0]["cases"][0]["state"] == "failed"
412+
event = client_q.get_nowait()
413+
assert event["node_type"] == "case"
414+
415+
def test_step_level_update(self):
416+
h, _ = _make_handler()
417+
h.is_running = True
418+
h.tree_state = {"suites": [{"cases": [{"steps": [{"state": "pending"}]}]}]}
419+
client_q = queue.Queue()
420+
h._clients.add(client_q)
421+
h.update_tree_node(state="passed", suite_idx=0, case_idx=0, step_idx=0)
422+
assert h.tree_state["suites"][0]["cases"][0]["steps"][0]["state"] == "passed"
423+
event = client_q.get_nowait()
424+
assert event["node_type"] == "step"
425+
426+
def test_tolerates_index_error_on_invalid_tree_state(self):
427+
h, _ = _make_handler()
428+
h.is_running = True
429+
h.tree_state = {} # missing expected structure
430+
h.update_tree_node(state="passed", suite_idx=99) # must not raise
431+
432+
433+
# ---------------------------------------------------------------------------
434+
# _build_tree()
435+
# ---------------------------------------------------------------------------
436+
437+
438+
@pytest.mark.unit
439+
class TestBuildTree:
440+
def test_minimal_run_no_suites(self):
441+
h, _ = _make_handler()
442+
run = MagicMock()
443+
run.title = "My Run"
444+
run.state = MagicMock(value="pending")
445+
run.test_suite_executions = []
446+
tree = h._build_tree(run)
447+
assert tree["title"] == "My Run"
448+
assert tree["suites"] == []
449+
450+
def test_builds_suite_case_step_hierarchy(self):
451+
h, _ = _make_handler()
452+
453+
step = MagicMock()
454+
step.title = "Step 1"
455+
step.state = MagicMock(value="pending")
456+
457+
case = MagicMock()
458+
case.test_case_metadata.title = "Case 1"
459+
case.test_case_metadata.public_id = "TC-1"
460+
case.state = MagicMock(value="pending")
461+
case.test_step_executions = [step]
462+
463+
suite = MagicMock()
464+
suite.test_suite_metadata.title = "Suite 1"
465+
suite.state = MagicMock(value="pending")
466+
suite.test_case_executions = [case]
467+
468+
run = MagicMock()
469+
run.title = "Run"
470+
run.state = MagicMock(value="executing")
471+
run.test_suite_executions = [suite]
472+
473+
tree = h._build_tree(run)
474+
assert len(tree["suites"]) == 1
475+
assert tree["suites"][0]["title"] == "Suite 1"
476+
assert len(tree["suites"][0]["cases"]) == 1
477+
assert len(tree["suites"][0]["cases"][0]["steps"]) == 1
478+
assert tree["suites"][0]["cases"][0]["steps"][0]["title"] == "Step 1"
479+
480+
def test_falls_back_to_default_titles_when_metadata_none(self):
481+
h, _ = _make_handler()
482+
483+
case = MagicMock()
484+
case.test_case_metadata = None
485+
case.state = MagicMock(value="pending")
486+
case.test_step_executions = []
487+
488+
suite = MagicMock()
489+
suite.test_suite_metadata = None
490+
suite.state = MagicMock(value="pending")
491+
suite.test_case_executions = [case]
492+
493+
run = MagicMock()
494+
run.title = "Run"
495+
run.state = MagicMock(value="pending")
496+
run.test_suite_executions = [suite]
497+
498+
tree = h._build_tree(run)
499+
assert "Suite 0" in tree["suites"][0]["title"]
500+
assert "Case 0" in tree["suites"][0]["cases"][0]["title"]
501+
502+
503+
# ---------------------------------------------------------------------------
504+
# _get_state()
505+
# ---------------------------------------------------------------------------
506+
507+
508+
@pytest.mark.unit
509+
class TestGetState:
510+
def test_returns_pending_when_state_is_none(self):
511+
h, _ = _make_handler()
512+
obj = MagicMock()
513+
obj.state = None
514+
assert h._get_state(obj) == "pending"
515+
516+
def test_returns_state_value_for_enum_like_object(self):
517+
h, _ = _make_handler()
518+
obj = MagicMock()
519+
obj.state = MagicMock(value="executing")
520+
assert h._get_state(obj) == "executing"
521+
522+
def test_returns_str_state_when_no_value_attr(self):
523+
h, _ = _make_handler()
524+
obj = MagicMock()
525+
obj.state = "passed" # plain string — no .value attribute
526+
assert h._get_state(obj) == "passed"
527+
528+
def test_returns_pending_on_exception(self):
529+
h, _ = _make_handler()
530+
531+
class BrokenState:
532+
@property
533+
def value(self):
534+
raise RuntimeError("broken")
535+
536+
obj = MagicMock()
537+
obj.state = BrokenState()
538+
assert h._get_state(obj) == "pending"
539+

0 commit comments

Comments
 (0)