2025-11-18 15:14:16 +00:00
|
|
|
"""Tests for sampling profiler CLI argument parsing and functionality."""
|
|
|
|
|
|
|
|
|
|
import io
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import _remote_debugging # noqa: F401
|
|
|
|
|
except ImportError:
|
|
|
|
|
raise unittest.SkipTest(
|
|
|
|
|
"Test only runs when _remote_debugging is available"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from test.support import is_emscripten
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSampleProfilerCLI(unittest.TestCase):
|
|
|
|
|
def _setup_sync_mocks(self, mock_socket, mock_popen):
|
|
|
|
|
"""Helper to set up socket and process mocks for coordinator tests."""
|
|
|
|
|
# Mock the sync socket with context manager support
|
|
|
|
|
mock_sock_instance = mock.MagicMock()
|
|
|
|
|
mock_sock_instance.getsockname.return_value = ("127.0.0.1", 12345)
|
|
|
|
|
|
|
|
|
|
# Mock the connection with context manager support
|
|
|
|
|
mock_conn = mock.MagicMock()
|
|
|
|
|
mock_conn.recv.return_value = b"ready"
|
|
|
|
|
mock_conn.__enter__.return_value = mock_conn
|
|
|
|
|
mock_conn.__exit__.return_value = None
|
|
|
|
|
|
|
|
|
|
# Mock accept() to return (connection, address) and support indexing
|
|
|
|
|
mock_accept_result = mock.MagicMock()
|
|
|
|
|
mock_accept_result.__getitem__.return_value = (
|
|
|
|
|
mock_conn # [0] returns the connection
|
|
|
|
|
)
|
|
|
|
|
mock_sock_instance.accept.return_value = mock_accept_result
|
|
|
|
|
|
|
|
|
|
# Mock socket with context manager support
|
|
|
|
|
mock_sock_instance.__enter__.return_value = mock_sock_instance
|
|
|
|
|
mock_sock_instance.__exit__.return_value = None
|
|
|
|
|
mock_socket.return_value = mock_sock_instance
|
|
|
|
|
|
|
|
|
|
# Mock the subprocess
|
|
|
|
|
mock_process = mock.MagicMock()
|
|
|
|
|
mock_process.pid = 12345
|
|
|
|
|
mock_process.poll.return_value = None
|
|
|
|
|
mock_popen.return_value = mock_process
|
|
|
|
|
return mock_process
|
|
|
|
|
|
|
|
|
|
def _verify_coordinator_command(self, mock_popen, expected_target_args):
|
|
|
|
|
"""Helper to verify the coordinator command was called correctly."""
|
|
|
|
|
args, kwargs = mock_popen.call_args
|
|
|
|
|
coordinator_cmd = args[0]
|
|
|
|
|
self.assertEqual(coordinator_cmd[0], sys.executable)
|
|
|
|
|
self.assertEqual(coordinator_cmd[1], "-m")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
coordinator_cmd[2], "profiling.sampling._sync_coordinator"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(coordinator_cmd[3], "12345") # port
|
|
|
|
|
# cwd is coordinator_cmd[4]
|
|
|
|
|
self.assertEqual(coordinator_cmd[5:], expected_target_args)
|
|
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_module_argument_parsing(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "run", "-m", "mymodule"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
2025-11-18 15:14:16 +00:00
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
|
2025-11-24 11:45:08 +00:00
|
|
|
# Verify sample was called once (exact arguments will vary with the new API)
|
|
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_module_with_arguments(self):
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
2025-11-18 15:14:16 +00:00
|
|
|
"-m",
|
|
|
|
|
"mymodule",
|
|
|
|
|
"arg1",
|
|
|
|
|
"arg2",
|
|
|
|
|
"--flag",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(
|
|
|
|
|
mock_popen, ("-m", "mymodule", "arg1", "arg2", "--flag")
|
|
|
|
|
)
|
2025-11-24 11:45:08 +00:00
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_script_argument_parsing(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "run", "myscript.py"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(mock_popen, ("myscript.py",))
|
2025-11-24 11:45:08 +00:00
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_script_with_arguments(self):
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
2025-11-18 15:14:16 +00:00
|
|
|
"myscript.py",
|
|
|
|
|
"arg1",
|
|
|
|
|
"arg2",
|
|
|
|
|
"--flag",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
# Use the helper to set up mocks consistently
|
|
|
|
|
mock_process = self._setup_sync_mocks(mock_socket, mock_popen)
|
|
|
|
|
# Override specific behavior for this test
|
|
|
|
|
mock_process.wait.side_effect = [
|
|
|
|
|
subprocess.TimeoutExpired(test_args, 0.1),
|
|
|
|
|
None,
|
|
|
|
|
]
|
|
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
# Verify the coordinator command was called
|
|
|
|
|
args, kwargs = mock_popen.call_args
|
|
|
|
|
coordinator_cmd = args[0]
|
|
|
|
|
self.assertEqual(coordinator_cmd[0], sys.executable)
|
|
|
|
|
self.assertEqual(coordinator_cmd[1], "-m")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
coordinator_cmd[2], "profiling.sampling._sync_coordinator"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(coordinator_cmd[3], "12345") # port
|
|
|
|
|
# cwd is coordinator_cmd[4]
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
coordinator_cmd[5:], ("myscript.py", "arg1", "arg2", "--flag")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_cli_mutually_exclusive_pid_module(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
# In new CLI, attach and run are separate subcommands, so this test
|
|
|
|
|
# verifies that mixing them causes an error
|
2025-11-18 15:14:16 +00:00
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach", # attach subcommand uses PID
|
2025-11-18 15:14:16 +00:00
|
|
|
"12345",
|
2025-11-24 11:45:08 +00:00
|
|
|
"-m", # -m is only for run subcommand
|
2025-11-18 15:14:16 +00:00
|
|
|
"mymodule",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
|
|
|
|
self.assertRaises(SystemExit) as cm,
|
|
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self.assertEqual(cm.exception.code, 2) # argparse error
|
|
|
|
|
error_msg = mock_stderr.getvalue()
|
2025-11-24 11:45:08 +00:00
|
|
|
self.assertIn("unrecognized arguments", error_msg)
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_cli_mutually_exclusive_pid_script(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
# In new CLI, you can't mix attach (PID) with run (script)
|
|
|
|
|
# This would be caught by providing a PID to run subcommand
|
|
|
|
|
test_args = ["profiling.sampling.cli", "run", "12345"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
self.assertRaises(FileNotFoundError) as cm, # Expect FileNotFoundError, not SystemExit
|
2025-11-18 15:14:16 +00:00
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
|
|
|
|
# Override to raise FileNotFoundError for non-existent script
|
|
|
|
|
mock_popen.side_effect = FileNotFoundError("12345")
|
|
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
# Verify the error is about the non-existent script
|
|
|
|
|
self.assertIn("12345", str(cm.exception))
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_cli_no_target_specified(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
# In new CLI, must specify a subcommand
|
|
|
|
|
test_args = ["profiling.sampling.cli", "-d", "5"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
|
|
|
|
self.assertRaises(SystemExit) as cm,
|
|
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self.assertEqual(cm.exception.code, 2) # argparse error
|
|
|
|
|
error_msg = mock_stderr.getvalue()
|
2025-11-24 11:45:08 +00:00
|
|
|
self.assertIn("invalid choice", error_msg)
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_module_with_profiler_options(self):
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
2025-11-18 15:14:16 +00:00
|
|
|
"-i",
|
|
|
|
|
"1000",
|
|
|
|
|
"-d",
|
|
|
|
|
"30",
|
|
|
|
|
"-a",
|
2025-11-24 11:45:08 +00:00
|
|
|
"--sort",
|
|
|
|
|
"tottime",
|
2025-11-18 15:14:16 +00:00
|
|
|
"-l",
|
|
|
|
|
"20",
|
|
|
|
|
"-m",
|
|
|
|
|
"mymodule",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
|
2025-11-24 11:45:08 +00:00
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_script_with_profiler_options(self):
|
|
|
|
|
"""Test script with various profiler options."""
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
2025-11-18 15:14:16 +00:00
|
|
|
"-i",
|
|
|
|
|
"2000",
|
|
|
|
|
"-d",
|
|
|
|
|
"60",
|
|
|
|
|
"--collapsed",
|
|
|
|
|
"-o",
|
|
|
|
|
"output.txt",
|
|
|
|
|
"myscript.py",
|
|
|
|
|
"scriptarg",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(
|
|
|
|
|
mock_popen, ("myscript.py", "scriptarg")
|
|
|
|
|
)
|
2025-11-24 11:45:08 +00:00
|
|
|
# Verify profiler was called
|
|
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_cli_empty_module_name(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "run", "-m"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
|
|
|
|
self.assertRaises(SystemExit) as cm,
|
|
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self.assertEqual(cm.exception.code, 2) # argparse error
|
|
|
|
|
error_msg = mock_stderr.getvalue()
|
2025-11-24 11:45:08 +00:00
|
|
|
self.assertIn("required: target", error_msg) # argparse error for missing positional arg
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
|
|
|
|
|
def test_cli_long_module_option(self):
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
|
|
|
|
"-m",
|
2025-11-18 15:14:16 +00:00
|
|
|
"mymodule",
|
|
|
|
|
"arg1",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("subprocess.Popen") as mock_popen,
|
|
|
|
|
mock.patch("socket.socket") as mock_socket,
|
|
|
|
|
):
|
|
|
|
|
self._setup_sync_mocks(mock_socket, mock_popen)
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self._verify_coordinator_command(
|
|
|
|
|
mock_popen, ("-m", "mymodule", "arg1")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_cli_complex_script_arguments(self):
|
|
|
|
|
test_args = [
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"run",
|
2025-11-18 15:14:16 +00:00
|
|
|
"script.py",
|
|
|
|
|
"--input",
|
|
|
|
|
"file.txt",
|
|
|
|
|
"-v",
|
|
|
|
|
"--output=/tmp/out",
|
|
|
|
|
"positional",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch(
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli._run_with_sync"
|
2025-11-18 15:14:16 +00:00
|
|
|
) as mock_run_with_sync,
|
|
|
|
|
):
|
|
|
|
|
mock_process = mock.MagicMock()
|
|
|
|
|
mock_process.pid = 12345
|
|
|
|
|
mock_process.wait.side_effect = [
|
|
|
|
|
subprocess.TimeoutExpired(test_args, 0.1),
|
|
|
|
|
None,
|
|
|
|
|
]
|
|
|
|
|
mock_process.poll.return_value = None
|
|
|
|
|
mock_run_with_sync.return_value = mock_process
|
|
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
mock_run_with_sync.assert_called_once_with(
|
|
|
|
|
(
|
|
|
|
|
sys.executable,
|
|
|
|
|
"script.py",
|
|
|
|
|
"--input",
|
|
|
|
|
"file.txt",
|
|
|
|
|
"-v",
|
|
|
|
|
"--output=/tmp/out",
|
|
|
|
|
"positional",
|
2025-11-20 18:27:17 +00:00
|
|
|
),
|
|
|
|
|
suppress_output=False
|
2025-11-18 15:14:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_cli_collapsed_format_validation(self):
|
|
|
|
|
"""Test that CLI properly validates incompatible options with collapsed format."""
|
|
|
|
|
test_cases = [
|
2025-11-24 11:45:08 +00:00
|
|
|
# Test sort option is invalid with collapsed
|
2025-11-18 15:14:16 +00:00
|
|
|
(
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
2025-11-18 15:14:16 +00:00
|
|
|
"12345",
|
|
|
|
|
"--collapsed",
|
2025-11-24 11:45:08 +00:00
|
|
|
"--sort",
|
|
|
|
|
"tottime", # Changed from nsamples (default) to trigger validation
|
2025-11-18 15:14:16 +00:00
|
|
|
],
|
|
|
|
|
"sort",
|
|
|
|
|
),
|
|
|
|
|
# Test limit option is invalid with collapsed
|
|
|
|
|
(
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
2025-11-18 15:14:16 +00:00
|
|
|
"12345",
|
|
|
|
|
"--collapsed",
|
2025-11-24 11:45:08 +00:00
|
|
|
"-l",
|
2025-11-18 15:14:16 +00:00
|
|
|
"20",
|
|
|
|
|
],
|
|
|
|
|
"limit",
|
|
|
|
|
),
|
|
|
|
|
# Test no-summary option is invalid with collapsed
|
|
|
|
|
(
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
|
|
|
|
"12345",
|
2025-11-18 15:14:16 +00:00
|
|
|
"--collapsed",
|
|
|
|
|
"--no-summary",
|
|
|
|
|
],
|
|
|
|
|
"summary",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
|
2025-11-18 15:14:16 +00:00
|
|
|
for test_args, expected_error_keyword in test_cases:
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample"), # Prevent actual profiling
|
2025-11-18 15:14:16 +00:00
|
|
|
self.assertRaises(SystemExit) as cm,
|
|
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
self.assertEqual(cm.exception.code, 2) # argparse error code
|
|
|
|
|
error_msg = mock_stderr.getvalue()
|
|
|
|
|
self.assertIn("error:", error_msg)
|
2025-11-24 11:45:08 +00:00
|
|
|
self.assertIn("only valid with --pstats", error_msg)
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_cli_default_collapsed_filename(self):
|
|
|
|
|
"""Test that collapsed format gets a default filename when not specified."""
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "attach", "12345", "--collapsed"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
# Check that sample was called (exact filename depends on implementation)
|
2025-11-18 15:14:16 +00:00
|
|
|
mock_sample.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_cli_custom_output_filenames(self):
|
|
|
|
|
"""Test custom output filenames for both formats."""
|
|
|
|
|
test_cases = [
|
|
|
|
|
(
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
|
|
|
|
"12345",
|
2025-11-18 15:14:16 +00:00
|
|
|
"--pstats",
|
|
|
|
|
"-o",
|
|
|
|
|
"custom.pstats",
|
|
|
|
|
],
|
|
|
|
|
"custom.pstats",
|
|
|
|
|
"pstats",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
|
|
|
|
"12345",
|
2025-11-18 15:14:16 +00:00
|
|
|
"--collapsed",
|
|
|
|
|
"-o",
|
|
|
|
|
"custom.txt",
|
|
|
|
|
],
|
|
|
|
|
"custom.txt",
|
|
|
|
|
"collapsed",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
|
2025-11-18 15:14:16 +00:00
|
|
|
for test_args, expected_filename, expected_format in test_cases:
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
mock_sample.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_cli_missing_required_arguments(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
"""Test that CLI requires subcommand."""
|
2025-11-18 15:14:16 +00:00
|
|
|
with (
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("sys.argv", ["profiling.sampling.cli"]),
|
2025-11-18 15:14:16 +00:00
|
|
|
mock.patch("sys.stderr", io.StringIO()),
|
|
|
|
|
):
|
|
|
|
|
with self.assertRaises(SystemExit):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_cli_mutually_exclusive_format_options(self):
|
|
|
|
|
"""Test that pstats and collapsed options are mutually exclusive."""
|
|
|
|
|
with (
|
|
|
|
|
mock.patch(
|
|
|
|
|
"sys.argv",
|
|
|
|
|
[
|
2025-11-24 11:45:08 +00:00
|
|
|
"profiling.sampling.cli",
|
|
|
|
|
"attach",
|
|
|
|
|
"12345",
|
2025-11-18 15:14:16 +00:00
|
|
|
"--pstats",
|
|
|
|
|
"--collapsed",
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
mock.patch("sys.stderr", io.StringIO()),
|
|
|
|
|
):
|
|
|
|
|
with self.assertRaises(SystemExit):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_argument_parsing_basic(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "attach", "12345"]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
mock_sample.assert_called_once()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
def test_sort_options(self):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
|
2025-11-18 15:14:16 +00:00
|
|
|
sort_options = [
|
2025-11-24 11:45:08 +00:00
|
|
|
("nsamples", 0),
|
|
|
|
|
("tottime", 1),
|
|
|
|
|
("cumtime", 2),
|
|
|
|
|
("sample-pct", 3),
|
|
|
|
|
("cumul-pct", 4),
|
|
|
|
|
("name", -1),
|
2025-11-18 15:14:16 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for option, expected_sort_value in sort_options:
|
2025-11-24 11:45:08 +00:00
|
|
|
test_args = ["profiling.sampling.cli", "attach", "12345", "--sort", option]
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
mock.patch("sys.argv", test_args),
|
2025-11-24 11:45:08 +00:00
|
|
|
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
2025-11-18 15:14:16 +00:00
|
|
|
):
|
2025-11-24 11:45:08 +00:00
|
|
|
from profiling.sampling.cli import main
|
|
|
|
|
main()
|
2025-11-18 15:14:16 +00:00
|
|
|
|
|
|
|
|
mock_sample.assert_called_once()
|
|
|
|
|
mock_sample.reset_mock()
|