From 07971e10b52bb83ac068b2156ccf322b42cdc06c Mon Sep 17 00:00:00 2001 From: Shaurya Bisht <87357655+ShauryaDusht@users.noreply.github.com> Date: Fri, 16 May 2025 23:27:07 +0530 Subject: [PATCH 1/5] gh-131178: Add CLI tests for profile module --- Lib/profile.py | 4 +- Lib/test/test_profile.py | 107 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/Lib/profile.py b/Lib/profile.py index a5afb12c9d121a..5dbb448aea6108 100644 --- a/Lib/profile.py +++ b/Lib/profile.py @@ -551,7 +551,7 @@ def f(m, f1=f1): #**************************************************************************** -def main(): +def main(args=None): import os from optparse import OptionParser @@ -570,7 +570,7 @@ def main(): parser.print_usage() sys.exit(2) - (options, args) = parser.parse_args() + (options, args) = parser.parse_args(args) sys.argv[:] = args # The script that we're profiling may chdir, so capture the absolute path diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py index 0f16b92334999c..2c662abbc591c3 100644 --- a/Lib/test/test_profile.py +++ b/Lib/test/test_profile.py @@ -4,6 +4,7 @@ import pstats import unittest import os +import subprocess from difflib import unified_diff from io import StringIO from test.support.os_helper import TESTFN, unlink, temp_dir, change_cwd @@ -130,6 +131,112 @@ def test_output_file_when_changing_directory(self): self.assertTrue(os.path.exists('out.pstats')) +class ProfileCLITests(unittest.TestCase): + """Tests for the profile module's command line interface.""" + + def setUp(self): + # Create a simple Python script to profile + self.script_content = """\ +def factorial(n): + if n <= 1: + return 1 + return n * factorial(n-1) + +if __name__ == "__main__": + factorial(10) +""" + self.script_file = TESTFN + with open(self.script_file, "w") as f: + f.write(self.script_content) + self.addCleanup(unlink, self.script_file) + + def _run_profile_cli(self, *args): + """Helper to run the profile CLI with given arguments.""" + cmd = [sys.executable, '-m', 'profile'] + list(args) + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True + ) + stdout, stderr = proc.communicate() + return proc.returncode, stdout, stderr + + def test_basic_profile(self): + """Test basic profiling of a script.""" + returncode, stdout, stderr = self._run_profile_cli(self.script_file) + self.assertEqual(returncode, 0) + self.assertIn("function calls", stdout) + self.assertIn("factorial", stdout) + + def test_sort_options(self): + """Test different sort options.""" + # List of sort options known to work + sort_options = ['calls', 'cumulative', 'cumtime', 'file', + 'filename', 'module', 'ncalls', 'pcalls', + 'line', 'stdname', 'time', 'tottime'] + + # Test each sort option individually + for option in sort_options: + with self.subTest(sort_option=option): + returncode, stdout, stderr = self._run_profile_cli( + '-s', option, self.script_file + ) + self.assertEqual(returncode, 0) + self.assertIn("function calls", stdout) + + def test_output_file(self): + """Test writing profile results to a file.""" + output_file = TESTFN + '.prof' + self.addCleanup(unlink, output_file) + + returncode, stdout, stderr = self._run_profile_cli( + '-o', output_file, self.script_file + ) + self.assertEqual(returncode, 0) + + # Check that the output file exists and contains profile data + self.assertTrue(os.path.exists(output_file)) + stats = pstats.Stats(output_file) + self.assertGreater(stats.total_calls, 0) + + def test_invalid_option(self): + """Test behavior with an invalid option.""" + returncode, stdout, stderr = self._run_profile_cli( + '--invalid-option', self.script_file + ) + self.assertNotEqual(returncode, 0) + self.assertIn("error", stderr.lower()) + + def test_no_arguments(self): + """Test behavior with no arguments.""" + returncode, stdout, stderr = self._run_profile_cli() + self.assertNotEqual(returncode, 0) + + # Check either stdout or stderr for usage information + combined_output = stdout.lower() + stderr.lower() + self.assertTrue("usage:" in combined_output or + "error:" in combined_output or + "no script filename specified" in combined_output, + "Expected usage information or error message not found") + + def test_run_module(self): + """Test profiling a module with -m option.""" + # Create a small module + module_name = "test_profile_module" + module_file = f"{module_name}.py" + + with open(module_file, "w") as f: + f.write("print('Module executed')\n") + + self.addCleanup(unlink, module_file) + + returncode, stdout, stderr = self._run_profile_cli( + '-m', module_name + ) + self.assertEqual(returncode, 0) + self.assertIn("Module executed", stdout) + self.assertIn("function calls", stdout) def regenerate_expected_output(filename, cls): filename = filename.rstrip('co') From fd894ec9928dccfde970a80d744ce734a4537290 Mon Sep 17 00:00:00 2001 From: Shaurya Bisht <87357655+ShauryaDusht@users.noreply.github.com> Date: Fri, 16 May 2025 23:46:37 +0530 Subject: [PATCH 2/5] Lint fixes --- Lib/test/test_profile.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py index 2c662abbc591c3..a92aaa6f98ada6 100644 --- a/Lib/test/test_profile.py +++ b/Lib/test/test_profile.py @@ -131,6 +131,7 @@ def test_output_file_when_changing_directory(self): self.assertTrue(os.path.exists('out.pstats')) + class ProfileCLITests(unittest.TestCase): """Tests for the profile module's command line interface.""" @@ -173,9 +174,9 @@ def test_sort_options(self): """Test different sort options.""" # List of sort options known to work sort_options = ['calls', 'cumulative', 'cumtime', 'file', - 'filename', 'module', 'ncalls', 'pcalls', - 'line', 'stdname', 'time', 'tottime'] - + 'filename', 'module', 'ncalls', 'pcalls', + 'line', 'stdname', 'time', 'tottime'] + # Test each sort option individually for option in sort_options: with self.subTest(sort_option=option): @@ -189,12 +190,12 @@ def test_output_file(self): """Test writing profile results to a file.""" output_file = TESTFN + '.prof' self.addCleanup(unlink, output_file) - + returncode, stdout, stderr = self._run_profile_cli( '-o', output_file, self.script_file ) self.assertEqual(returncode, 0) - + # Check that the output file exists and contains profile data self.assertTrue(os.path.exists(output_file)) stats = pstats.Stats(output_file) @@ -212,25 +213,27 @@ def test_no_arguments(self): """Test behavior with no arguments.""" returncode, stdout, stderr = self._run_profile_cli() self.assertNotEqual(returncode, 0) - + # Check either stdout or stderr for usage information combined_output = stdout.lower() + stderr.lower() - self.assertTrue("usage:" in combined_output or - "error:" in combined_output or - "no script filename specified" in combined_output, - "Expected usage information or error message not found") + self.assertTrue( + "usage:" in combined_output or + "error:" in combined_output or + "no script filename specified" in combined_output, + "Expected usage information or error message not found" + ) def test_run_module(self): """Test profiling a module with -m option.""" # Create a small module module_name = "test_profile_module" module_file = f"{module_name}.py" - + with open(module_file, "w") as f: f.write("print('Module executed')\n") - + self.addCleanup(unlink, module_file) - + returncode, stdout, stderr = self._run_profile_cli( '-m', module_name ) @@ -238,6 +241,7 @@ def test_run_module(self): self.assertIn("Module executed", stdout) self.assertIn("function calls", stdout) + def regenerate_expected_output(filename, cls): filename = filename.rstrip('co') print('Regenerating %s...' % filename) From 5501828500edac98b6a82606a9286d77dd24fa60 Mon Sep 17 00:00:00 2001 From: Shaurya Bisht <87357655+ShauryaDusht@users.noreply.github.com> Date: Sat, 17 May 2025 00:19:42 +0530 Subject: [PATCH 3/5] remove args --- Lib/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/profile.py b/Lib/profile.py index 5dbb448aea6108..a5afb12c9d121a 100644 --- a/Lib/profile.py +++ b/Lib/profile.py @@ -551,7 +551,7 @@ def f(m, f1=f1): #**************************************************************************** -def main(args=None): +def main(): import os from optparse import OptionParser @@ -570,7 +570,7 @@ def main(args=None): parser.print_usage() sys.exit(2) - (options, args) = parser.parse_args(args) + (options, args) = parser.parse_args() sys.argv[:] = args # The script that we're profiling may chdir, so capture the absolute path From e7887f0b354d94e3888e03324b4b98180026f335 Mon Sep 17 00:00:00 2001 From: Shaurya Bisht <87357655+ShauryaDusht@users.noreply.github.com> Date: Sat, 17 May 2025 12:08:06 +0530 Subject: [PATCH 4/5] removed comments and minor fixes --- Lib/test/test_profile.py | 56 +++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py index a92aaa6f98ada6..a9c5adbb6406ef 100644 --- a/Lib/test/test_profile.py +++ b/Lib/test/test_profile.py @@ -5,6 +5,8 @@ import unittest import os import subprocess +import tempfile +import shutil from difflib import unified_diff from io import StringIO from test.support.os_helper import TESTFN, unlink, temp_dir, change_cwd @@ -136,7 +138,9 @@ class ProfileCLITests(unittest.TestCase): """Tests for the profile module's command line interface.""" def setUp(self): - # Create a simple Python script to profile + self.temp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.temp_dir) + self.script_content = """\ def factorial(n): if n <= 1: @@ -146,14 +150,12 @@ def factorial(n): if __name__ == "__main__": factorial(10) """ - self.script_file = TESTFN + self.script_file = os.path.join(self.temp_dir, "factorial_script.py") with open(self.script_file, "w") as f: f.write(self.script_content) - self.addCleanup(unlink, self.script_file) def _run_profile_cli(self, *args): - """Helper to run the profile CLI with given arguments.""" - cmd = [sys.executable, '-m', 'profile'] + list(args) + cmd = [sys.executable, '-m', 'profile', *args] proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -164,20 +166,17 @@ def _run_profile_cli(self, *args): return proc.returncode, stdout, stderr def test_basic_profile(self): - """Test basic profiling of a script.""" returncode, stdout, stderr = self._run_profile_cli(self.script_file) self.assertEqual(returncode, 0) self.assertIn("function calls", stdout) self.assertIn("factorial", stdout) + self.assertIn("ncalls", stdout) def test_sort_options(self): - """Test different sort options.""" - # List of sort options known to work sort_options = ['calls', 'cumulative', 'cumtime', 'file', 'filename', 'module', 'ncalls', 'pcalls', 'line', 'stdname', 'time', 'tottime'] - # Test each sort option individually for option in sort_options: with self.subTest(sort_option=option): returncode, stdout, stderr = self._run_profile_cli( @@ -187,22 +186,18 @@ def test_sort_options(self): self.assertIn("function calls", stdout) def test_output_file(self): - """Test writing profile results to a file.""" - output_file = TESTFN + '.prof' - self.addCleanup(unlink, output_file) + output_file = os.path.join(self.temp_dir, "profile_output.prof") returncode, stdout, stderr = self._run_profile_cli( '-o', output_file, self.script_file ) self.assertEqual(returncode, 0) - # Check that the output file exists and contains profile data self.assertTrue(os.path.exists(output_file)) stats = pstats.Stats(output_file) self.assertGreater(stats.total_calls, 0) def test_invalid_option(self): - """Test behavior with an invalid option.""" returncode, stdout, stderr = self._run_profile_cli( '--invalid-option', self.script_file ) @@ -210,11 +205,9 @@ def test_invalid_option(self): self.assertIn("error", stderr.lower()) def test_no_arguments(self): - """Test behavior with no arguments.""" returncode, stdout, stderr = self._run_profile_cli() self.assertNotEqual(returncode, 0) - # Check either stdout or stderr for usage information combined_output = stdout.lower() + stderr.lower() self.assertTrue( "usage:" in combined_output or @@ -224,19 +217,30 @@ def test_no_arguments(self): ) def test_run_module(self): - """Test profiling a module with -m option.""" - # Create a small module - module_name = "test_profile_module" - module_file = f"{module_name}.py" - + module_name = "profilemod" + module_file = os.path.join(self.temp_dir, f"{module_name}.py") + with open(module_file, "w") as f: f.write("print('Module executed')\n") - - self.addCleanup(unlink, module_file) - - returncode, stdout, stderr = self._run_profile_cli( - '-m', module_name + + env = os.environ.copy() + + python_path = self.temp_dir + if 'PYTHONPATH' in env: + python_path = os.pathsep.join([python_path, env['PYTHONPATH']]) + env['PYTHONPATH'] = python_path + + cmd = [sys.executable, '-m', 'profile', '-m', module_name] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env=env ) + stdout, stderr = proc.communicate() + returncode = proc.returncode + self.assertEqual(returncode, 0) self.assertIn("Module executed", stdout) self.assertIn("function calls", stdout) From 9d6e590371f1a75dbae6eefbc23cc344644886cd Mon Sep 17 00:00:00 2001 From: Shaurya Bisht <87357655+ShauryaDusht@users.noreply.github.com> Date: Sat, 17 May 2025 12:13:12 +0530 Subject: [PATCH 5/5] lint fixes --- Lib/test/test_profile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py index a9c5adbb6406ef..581b120a9a88cb 100644 --- a/Lib/test/test_profile.py +++ b/Lib/test/test_profile.py @@ -140,7 +140,7 @@ class ProfileCLITests(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.temp_dir) - + self.script_content = """\ def factorial(n): if n <= 1: @@ -219,17 +219,17 @@ def test_no_arguments(self): def test_run_module(self): module_name = "profilemod" module_file = os.path.join(self.temp_dir, f"{module_name}.py") - + with open(module_file, "w") as f: f.write("print('Module executed')\n") - + env = os.environ.copy() - + python_path = self.temp_dir if 'PYTHONPATH' in env: python_path = os.pathsep.join([python_path, env['PYTHONPATH']]) env['PYTHONPATH'] = python_path - + cmd = [sys.executable, '-m', 'profile', '-m', module_name] proc = subprocess.Popen( cmd, @@ -240,7 +240,7 @@ def test_run_module(self): ) stdout, stderr = proc.communicate() returncode = proc.returncode - + self.assertEqual(returncode, 0) self.assertIn("Module executed", stdout) self.assertIn("function calls", stdout)