"""Tests for backends.common subprocess/git helpers. The streaming mode of ``run_console_subprocess`` (used by the in-TUI task view) is exercised with a real child process: output lines are captured and forwarded, cancellation kills the child and returns 130, and an on_cancel hook runs first. """ import sys import threading import unittest from unittest import mock from backends import common class RunConsoleSubprocessStreamingTests(unittest.TestCase): def test_streaming_emits_merged_lines(self): lines = [] rc = common.run_console_subprocess( [sys.executable, "-c", "import sys; sys.stdout.write('hello\\nworld\\n'); " "sys.stderr.write('oops\\n')"], emit=lines.append) self.assertEqual(rc, 0) # stdout/stderr are merged, in arrival order. self.assertEqual(sorted(lines), ["hello", "oops", "world"]) def test_streaming_returns_the_exit_code(self): rc = common.run_console_subprocess( [sys.executable, "-c", "import sys; sys.exit(3)"], emit=lambda line: None) self.assertEqual(rc, 3) def test_cancel_kills_the_process_and_returns_130(self): cancel = threading.Event() cancel.set() rc = common.run_console_subprocess( [sys.executable, "-c", "import time; time.sleep(60)"], emit=lambda line: None, cancel=cancel) self.assertEqual(rc, 130) def test_on_cancel_hook_runs_before_kill(self): cancel = threading.Event() cancel.set() touched = [] common.run_console_subprocess( [sys.executable, "-c", "import time; time.sleep(60)"], emit=lambda line: None, cancel=cancel, on_cancel=lambda: touched.append(True)) self.assertEqual(touched, [True]) class GitCloneTests(unittest.TestCase): def test_git_clone_console_passes_through(self): with mock.patch.object(common, "run_console_subprocess", return_value=0) as run: self.assertEqual(common.git_clone("url", common.Path("/t")), 0) # Console mode: no --progress flag, plain git clone. self.assertEqual(run.call_args[0][0], ["git", "clone", "url", "/t"]) def test_git_clone_streaming_adds_progress(self): def emit(line): pass with mock.patch.object(common, "run_console_subprocess", return_value=0) as run: self.assertEqual(common.git_clone("url", common.Path("/t"), emit=emit), 0) argv = run.call_args[0][0] self.assertEqual(argv[:3], ["git", "clone", "--progress"]) self.assertIn("url", argv) self.assertEqual(run.call_args[1]["emit"], emit) class ParseRequestOptionsTests(unittest.TestCase): """parse_request_options: the shared --option / TUI-field parser.""" def test_single_item(self): self.assertEqual(common.parse_request_options("speed=1.1"), {"speed": "1.1"}) def test_comma_and_whitespace_separators_mix(self): self.assertEqual( common.parse_request_options("emotion=neutral, speed=1.1"), {"emotion": "neutral", "speed": "1.1"}) self.assertEqual( common.parse_request_options("a=1 b=2\tc=3"), {"a": "1", "b": "2", "c": "3"}) def test_keys_are_stripped_and_blank_text_is_empty(self): self.assertEqual(common.parse_request_options(" "), {}) self.assertEqual(common.parse_request_options(""), {}) # Tokens cannot contain whitespace (items split on it), so a lone # "=" with a blank key is the malformed case, caught below. self.assertEqual(common.parse_request_options("speed=1"), {"speed": "1"}) def test_value_is_kept_verbatim(self): self.assertEqual( common.parse_request_options("url=http://x:8080/path?a=1"), {"url": "http://x:8080/path?a=1"}) def test_later_duplicates_override_earlier_ones(self): self.assertEqual(common.parse_request_options("a=1,a=2"), {"a": "2"}) def test_item_without_equals_is_rejected(self): with self.assertRaises(ValueError): common.parse_request_options("emotion=neutral nonsense") def test_item_with_a_blank_key_is_rejected(self): with self.assertRaises(ValueError): common.parse_request_options("=value") if __name__ == "__main__": unittest.main()