gh-138122: Refactor the CLI of profiling.sampling into subcommands (#141813)

This commit is contained in:
Pablo Galindo Salgado 2025-11-24 11:45:08 +00:00 committed by GitHub
parent 425f24e4fa
commit 3eec46d3c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1460 additions and 1440 deletions

View file

@ -25,8 +25,6 @@
from test.support import (
requires_subprocess,
captured_stdout,
captured_stderr,
SHORT_TIMEOUT,
)
@ -293,7 +291,7 @@ def test_alternating_call_patterns(self):
def test_collapsed_stack_with_recursion(self):
"""Test collapsed stack collector with recursive patterns."""
collector = CollapsedStackCollector()
collector = CollapsedStackCollector(1000)
# Recursive call pattern
recursive_frames = [
@ -407,12 +405,13 @@ def test_sampling_basic_functionality(self):
):
try:
# Sample for up to SHORT_TIMEOUT seconds, but process exits after fixed iterations
collector = PstatsCollector(sample_interval_usec=1000, skip_idle=False)
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=SHORT_TIMEOUT,
sample_interval_usec=1000, # 1ms
show_summary=False,
)
collector.print_stats(show_summary=False)
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@ -439,12 +438,13 @@ def test_sampling_with_pstats_export(self):
mock.patch("sys.stdout", captured_output),
):
try:
collector = PstatsCollector(sample_interval_usec=10000, skip_idle=False)
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=1,
filename=pstats_out.name,
sample_interval_usec=10000,
)
collector.export(pstats_out.name)
except PermissionError:
self.skipTest(
"Insufficient permissions for remote profiling"
@ -484,13 +484,13 @@ def test_sampling_with_collapsed_export(self):
mock.patch("sys.stdout", captured_output),
):
try:
collector = CollapsedStackCollector(1000, skip_idle=False)
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=1,
filename=collapsed_file.name,
output_format="collapsed",
sample_interval_usec=10000,
)
collector.export(collapsed_file.name)
except PermissionError:
self.skipTest(
"Insufficient permissions for remote profiling"
@ -532,13 +532,14 @@ def test_sampling_all_threads(self):
mock.patch("sys.stdout", captured_output),
):
try:
collector = PstatsCollector(sample_interval_usec=10000, skip_idle=False)
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=1,
all_threads=True,
sample_interval_usec=10000,
show_summary=False,
)
collector.print_stats(show_summary=False)
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@ -552,7 +553,7 @@ def test_sample_target_script(self):
self.addCleanup(close_and_unlink, script_file)
# Sample for up to SHORT_TIMEOUT seconds, but process exits after fixed iterations
test_args = ["profiling.sampling.sample", "-d", PROFILING_TIMEOUT, script_file.name]
test_args = ["profiling.sampling.sample", "run", "-d", PROFILING_TIMEOUT, script_file.name]
with (
mock.patch("sys.argv", test_args),
@ -560,7 +561,8 @@ def test_sample_target_script(self):
mock.patch("sys.stdout", captured_output),
):
try:
profiling.sampling.sample.main()
from profiling.sampling.cli import main
main()
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@ -584,7 +586,8 @@ def test_sample_target_module(self):
f.write(self.test_script)
test_args = [
"profiling.sampling.sample",
"profiling.sampling.cli",
"run",
"-d",
PROFILING_TIMEOUT,
"-m",
@ -599,7 +602,8 @@ def test_sample_target_module(self):
contextlib.chdir(tempdir.name),
):
try:
profiling.sampling.sample.main()
from profiling.sampling.cli import main
main()
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@ -622,7 +626,8 @@ def test_sample_target_module(self):
class TestSampleProfilerErrorHandling(unittest.TestCase):
def test_invalid_pid(self):
with self.assertRaises((OSError, RuntimeError)):
profiling.sampling.sample.sample(-1, duration_sec=1)
collector = PstatsCollector(sample_interval_usec=100, skip_idle=False)
profiling.sampling.sample.sample(-1, collector, duration_sec=1)
def test_process_dies_during_sampling(self):
with test_subprocess(
@ -633,10 +638,11 @@ def test_process_dies_during_sampling(self):
mock.patch("sys.stdout", captured_output),
):
try:
collector = PstatsCollector(sample_interval_usec=50000, skip_idle=False)
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=2, # Longer than process lifetime
sample_interval_usec=50000,
)
except PermissionError:
self.skipTest(
@ -647,34 +653,6 @@ def test_process_dies_during_sampling(self):
self.assertIn("Error rate", output)
def test_invalid_output_format(self):
with self.assertRaises(ValueError):
profiling.sampling.sample.sample(
os.getpid(),
duration_sec=1,
output_format="invalid_format",
)
def test_invalid_output_format_with_mocked_profiler(self):
"""Test invalid output format with proper mocking to avoid permission issues."""
with mock.patch(
"profiling.sampling.sample.SampleProfiler"
) as mock_profiler_class:
mock_profiler = mock.MagicMock()
mock_profiler_class.return_value = mock_profiler
with self.assertRaises(ValueError) as cm:
profiling.sampling.sample.sample(
12345,
duration_sec=1,
output_format="unknown_format",
)
# Should raise ValueError with the invalid format name
self.assertIn(
"Invalid output format: unknown_format", str(cm.exception)
)
def test_is_process_running(self):
with test_subprocess("import time; time.sleep(1000)") as subproc:
try:
@ -723,31 +701,6 @@ def test_esrch_signal_handling(self):
with self.assertRaises(ProcessLookupError):
unwinder.get_stack_trace()
def test_valid_output_formats(self):
"""Test that all valid output formats are accepted."""
valid_formats = ["pstats", "collapsed", "flamegraph", "gecko"]
tempdir = tempfile.TemporaryDirectory(delete=False)
self.addCleanup(shutil.rmtree, tempdir.name)
with (
contextlib.chdir(tempdir.name),
captured_stdout(),
captured_stderr(),
):
for fmt in valid_formats:
try:
# This will likely fail with permissions, but the format should be valid
profiling.sampling.sample.sample(
os.getpid(),
duration_sec=0.1,
output_format=fmt,
filename=f"test_{fmt}.out",
)
except (OSError, RuntimeError, PermissionError):
# Expected errors - we just want to test format validation
pass
def test_script_error_treatment(self):
script_file = tempfile.NamedTemporaryFile(
"w", delete=False, suffix=".py"
@ -760,7 +713,8 @@ def test_script_error_treatment(self):
[
sys.executable,
"-m",
"profiling.sampling.sample",
"profiling.sampling.cli",
"run",
"-d",
"1",
script_file.name,
@ -770,9 +724,59 @@ def test_script_error_treatment(self):
)
output = result.stdout + result.stderr
if "PermissionError" in output:
if "Permission Error" in output:
self.skipTest("Insufficient permissions for remote profiling")
self.assertNotIn("Script file not found", output)
self.assertIn(
"No such file or directory: 'nonexistent_file.txt'", output
)
def test_live_incompatible_with_pstats_options(self):
"""Test that --live is incompatible with individual pstats options."""
test_cases = [
(["--sort", "tottime"], "--sort"),
(["--limit", "30"], "--limit"),
(["--no-summary"], "--no-summary"),
]
for args, expected_flag in test_cases:
with self.subTest(args=args):
test_args = ["profiling.sampling.cli", "run", "--live"] + args + ["test.py"]
with mock.patch("sys.argv", test_args):
with self.assertRaises(SystemExit) as cm:
from profiling.sampling.cli import main
main()
self.assertNotEqual(cm.exception.code, 0)
def test_live_incompatible_with_multiple_pstats_options(self):
"""Test that --live is incompatible with multiple pstats options."""
test_args = [
"profiling.sampling.cli", "run", "--live",
"--sort", "cumtime", "--limit", "25", "--no-summary", "test.py"
]
with mock.patch("sys.argv", test_args):
with self.assertRaises(SystemExit) as cm:
from profiling.sampling.cli import main
main()
self.assertNotEqual(cm.exception.code, 0)
def test_live_incompatible_with_pstats_default_values(self):
"""Test that --live blocks pstats options even with default values."""
# Test with --sort=nsamples (the default value)
test_args = ["profiling.sampling.cli", "run", "--live", "--sort=nsamples", "test.py"]
with mock.patch("sys.argv", test_args):
with self.assertRaises(SystemExit) as cm:
from profiling.sampling.cli import main
main()
self.assertNotEqual(cm.exception.code, 0)
# Test with --limit=15 (the default value)
test_args = ["profiling.sampling.cli", "run", "--live", "--limit=15", "test.py"]
with mock.patch("sys.argv", test_args):
with self.assertRaises(SystemExit) as cm:
from profiling.sampling.cli import main
main()
self.assertNotEqual(cm.exception.code, 0)