|
1 | 1 | """Tests for sampling profiler CLI argument parsing and functionality.""" |
2 | 2 |
|
| 3 | +import functools |
3 | 4 | import io |
4 | 5 | import subprocess |
5 | 6 | import sys |
| 7 | +import tempfile |
6 | 8 | import unittest |
7 | 9 | from unittest import mock |
8 | 10 |
|
|
17 | 19 |
|
18 | 20 | from profiling.sampling.cli import main |
19 | 21 | from profiling.sampling.errors import SamplingScriptNotFoundError, SamplingModuleNotFoundError, SamplingUnknownProcessError |
| 22 | +from profiling.sampling.live_collector import MockDisplay, LiveStatsCollector |
20 | 23 |
|
21 | 24 |
|
22 | 25 | class TestSampleProfilerCLI(unittest.TestCase): |
@@ -722,3 +725,74 @@ def test_cli_attach_nonexistent_pid(self): |
722 | 725 | main() |
723 | 726 |
|
724 | 727 | self.assertIn(fake_pid, str(cm.exception)) |
| 728 | + |
| 729 | + |
| 730 | +class TestLiveModeErrors(unittest.TestCase): |
| 731 | + """Tests running error commands in the live mode fails gracefully.""" |
| 732 | + |
| 733 | + def mock_curses_wrapper(self, func): |
| 734 | + func(mock.MagicMock()) |
| 735 | + |
| 736 | + def mock_init_curses_side_effect(self, n_times, mock_self, stdscr): |
| 737 | + mock_self.display = MockDisplay() |
| 738 | + # Allow the loop to run for a bit (approx 0.5s) before quitting |
| 739 | + # This ensures we don't exit too early while the subprocess is |
| 740 | + # still failing |
| 741 | + for _ in range(n_times): |
| 742 | + mock_self.display.simulate_input(-1) |
| 743 | + if n_times >= 500: |
| 744 | + mock_self.display.simulate_input(ord('q')) |
| 745 | + |
| 746 | + @unittest.skipIf(is_emscripten, "subprocess not available") |
| 747 | + def test_run_failed_module_live(self): |
| 748 | + """Test that running a existing module that fails exists with clean error.""" |
| 749 | + |
| 750 | + args = [ |
| 751 | + "profiling.sampling.cli", "run", "--live", "-m", "test", |
| 752 | + "test_asdasd" |
| 753 | + ] |
| 754 | + |
| 755 | + with ( |
| 756 | + mock.patch( |
| 757 | + 'profiling.sampling.live_collector.collector.LiveStatsCollector.init_curses', |
| 758 | + autospec=True, |
| 759 | + side_effect=functools.partial(self.mock_init_curses_side_effect, 750) |
| 760 | + ), |
| 761 | + mock.patch('curses.wrapper', side_effect=self.mock_curses_wrapper), |
| 762 | + mock.patch("sys.argv", args), |
| 763 | + mock.patch('sys.stderr', new=io.StringIO()) as fake_stderr |
| 764 | + ): |
| 765 | + main() |
| 766 | + self.assertStartsWith( |
| 767 | + fake_stderr.getvalue(), |
| 768 | + '\x1b[31mtest test_asdasd crashed -- Traceback (most recent call last):' |
| 769 | + ) |
| 770 | + |
| 771 | + @unittest.skipIf(is_emscripten, "subprocess not available") |
| 772 | + def test_run_failed_script_live(self): |
| 773 | + """Test that running a failing script exits with clean error.""" |
| 774 | + script = tempfile.NamedTemporaryFile(suffix=".py") |
| 775 | + script.write(b'1/0\n') |
| 776 | + script.seek(0) |
| 777 | + |
| 778 | + args = ["profiling.sampling.cli", "run", "--live", script.name] |
| 779 | + |
| 780 | + with ( |
| 781 | + mock.patch( |
| 782 | + 'profiling.sampling.live_collector.collector.LiveStatsCollector.init_curses', |
| 783 | + autospec=True, |
| 784 | + side_effect=functools.partial(self.mock_init_curses_side_effect, 200) |
| 785 | + ), |
| 786 | + mock.patch('curses.wrapper', side_effect=self.mock_curses_wrapper), |
| 787 | + mock.patch("sys.argv", args), |
| 788 | + mock.patch('sys.stderr', new=io.StringIO()) as fake_stderr |
| 789 | + ): |
| 790 | + main() |
| 791 | + stderr = fake_stderr.getvalue() |
| 792 | + self.assertIn( |
| 793 | + 'sample(s) collected (minimum 200 required for TUI)', stderr |
| 794 | + ) |
| 795 | + self.assertEndsWith( |
| 796 | + stderr, |
| 797 | + 'ZeroDivisionError\x1b[0m: \x1b[35mdivision by zero\x1b[0m\n\n' |
| 798 | + ) |
0 commit comments