|
1 | 1 | """Tests for keras_remote.backend.log_streaming — live pod log streaming.""" |
2 | 2 |
|
3 | | -import io |
4 | 3 | from unittest import mock |
5 | 4 | from unittest.mock import MagicMock |
6 | 5 |
|
|
9 | 8 |
|
10 | 9 | from keras_remote.backend.log_streaming import ( |
11 | 10 | LogStreamer, |
12 | | - _render_live_panel, |
13 | | - _render_plain, |
14 | 11 | _stream_pod_logs, |
15 | 12 | ) |
16 | 13 |
|
@@ -40,33 +37,23 @@ def test_calls_log_api_correctly(self): |
40 | 37 | _preload_content=False, |
41 | 38 | ) |
42 | 39 |
|
43 | | - def test_routes_by_terminal(self): |
44 | | - for is_terminal in (True, False): |
45 | | - mock_core = MagicMock() |
46 | | - mock_core.read_namespaced_pod_log.return_value = self._make_mock_resp( |
47 | | - [b"hello\n"] |
48 | | - ) |
49 | | - |
50 | | - with ( |
51 | | - mock.patch( |
52 | | - "keras_remote.backend.log_streaming.Console" |
53 | | - ) as mock_console_cls, |
54 | | - mock.patch( |
55 | | - "keras_remote.backend.log_streaming._render_live_panel" |
56 | | - ) as mock_live, |
57 | | - mock.patch( |
58 | | - "keras_remote.backend.log_streaming._render_plain" |
59 | | - ) as mock_plain, |
60 | | - ): |
61 | | - mock_console_cls.return_value.is_terminal = is_terminal |
62 | | - _stream_pod_logs(mock_core, "pod-1", "default") |
63 | | - |
64 | | - if is_terminal: |
65 | | - mock_live.assert_called_once() |
66 | | - mock_plain.assert_not_called() |
67 | | - else: |
68 | | - mock_plain.assert_called_once() |
69 | | - mock_live.assert_not_called() |
| 40 | + def test_handles_partial_lines(self): |
| 41 | + mock_core = MagicMock() |
| 42 | + # "hello\nwor" then "ld\n" — "world" is split across chunks |
| 43 | + mock_core.read_namespaced_pod_log.return_value = self._make_mock_resp( |
| 44 | + [b"hello\nwor", b"ld\n"] |
| 45 | + ) |
| 46 | + |
| 47 | + with mock.patch( |
| 48 | + "keras_remote.backend.log_streaming.LiveOutputPanel" |
| 49 | + ) as mock_panel_cls: |
| 50 | + mock_panel = MagicMock() |
| 51 | + mock_panel_cls.return_value.__enter__ = MagicMock(return_value=mock_panel) |
| 52 | + mock_panel_cls.return_value.__exit__ = MagicMock(return_value=False) |
| 53 | + _stream_pod_logs(mock_core, "pod-1", "default") |
| 54 | + |
| 55 | + lines = [call[0][0] for call in mock_panel.on_output.call_args_list] |
| 56 | + self.assertEqual(lines, ["hello", "world"]) |
70 | 57 |
|
71 | 58 | def test_releases_conn_on_api_exception(self): |
72 | 59 | mock_core = MagicMock() |
@@ -111,66 +98,6 @@ def test_logs_warning_on_unexpected_error(self): |
111 | 98 | mock_resp.release_conn.assert_called_once() |
112 | 99 |
|
113 | 100 |
|
114 | | -class TestRenderPlain(absltest.TestCase): |
115 | | - """Tests for the non-terminal plain rendering path.""" |
116 | | - |
117 | | - def test_streams_chunks_to_stdout(self): |
118 | | - mock_resp = MagicMock() |
119 | | - mock_resp.stream.return_value = [b"line 1\n", b"line 2\n"] |
120 | | - console = MagicMock() |
121 | | - |
122 | | - with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: |
123 | | - _render_plain(mock_resp, "pod-1", console) |
124 | | - |
125 | | - self.assertIn("line 1", mock_stdout.getvalue()) |
126 | | - self.assertIn("line 2", mock_stdout.getvalue()) |
127 | | - |
128 | | - def test_prints_rule_delimiters(self): |
129 | | - mock_resp = MagicMock() |
130 | | - mock_resp.stream.return_value = [] |
131 | | - console = MagicMock() |
132 | | - |
133 | | - _render_plain(mock_resp, "pod-1", console) |
134 | | - |
135 | | - self.assertEqual(console.rule.call_count, 2) |
136 | | - # Opening rule contains pod name |
137 | | - self.assertIn("pod-1", console.rule.call_args_list[0][0][0]) |
138 | | - |
139 | | - def test_handles_utf8_decode_errors(self): |
140 | | - mock_resp = MagicMock() |
141 | | - mock_resp.stream.return_value = [b"valid\n", b"\xff\xfe invalid\n"] |
142 | | - console = MagicMock() |
143 | | - |
144 | | - with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: |
145 | | - _render_plain(mock_resp, "pod-1", console) |
146 | | - |
147 | | - output = mock_stdout.getvalue() |
148 | | - self.assertIn("valid", output) |
149 | | - self.assertIn("invalid", output) |
150 | | - |
151 | | - |
152 | | -class TestRenderLivePanel(absltest.TestCase): |
153 | | - """Tests for the terminal Live panel rendering path.""" |
154 | | - |
155 | | - def test_handles_partial_lines(self): |
156 | | - mock_resp = MagicMock() |
157 | | - # "hello\nwor" then "ld\n" — "world" is split across chunks |
158 | | - mock_resp.stream.return_value = [b"hello\nwor", b"ld\n"] |
159 | | - console = MagicMock() |
160 | | - |
161 | | - with mock.patch("keras_remote.backend.log_streaming.Live") as mock_live_cls: |
162 | | - mock_live = MagicMock() |
163 | | - mock_live_cls.return_value.__enter__ = MagicMock(return_value=mock_live) |
164 | | - mock_live_cls.return_value.__exit__ = MagicMock(return_value=False) |
165 | | - _render_live_panel(mock_resp, "pod-1", console) |
166 | | - |
167 | | - # Check the final panel contains both complete lines |
168 | | - last_panel = mock_live.update.call_args_list[-1][0][0] |
169 | | - panel_content = last_panel.renderable |
170 | | - self.assertIn("hello", panel_content) |
171 | | - self.assertIn("world", panel_content) |
172 | | - |
173 | | - |
174 | 101 | class TestLogStreamer(absltest.TestCase): |
175 | 102 | def test_start_launches_daemon_thread(self): |
176 | 103 | mock_core = MagicMock() |
|
0 commit comments