mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
gh-138122: Refactor the CLI of profiling.sampling into subcommands (#141813)
This commit is contained in:
parent
425f24e4fa
commit
3eec46d3c3
15 changed files with 1460 additions and 1440 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue