From 31c2aa78df815700d6e93645c203e7c648bfdbd1 Mon Sep 17 00:00:00 2001 From: rudy-07 Date: Sun, 1 Nov 2026 19:42:58 +0530 Subject: [PATCH 1/3] gh-131178: Add tests for ftplib command-line interface Add unit tests in Lib/test/test_ftplib.py covering the ftplib command-line interface: verifying usage message on empty invocation, subprocess invocation, host connection with debuglevel, directory and passive mode commands, file retrieval, and custom netrc file error handling. --- Lib/test/test_ftplib.py | 65 +++++++++++++++++++ ...-11-01-19-45-00.gh-issue-131178.ftplib.rst | 1 + 2 files changed, 66 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index 983a8b92cf6384d..e8592969f7ef373 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -1191,6 +1191,71 @@ def test__all__(self): support.check__all__(self, ftplib, not_exported=not_exported) +class CommandLineTest(TestCase): + """Tests for the ftplib command-line interface.""" + + def test_main_no_arguments_prints_usage(self): + with mock.patch('sys.argv', ['ftplib']), \ + support.captured_stdout() as stdout: + with self.assertRaises(SystemExit) as cm: + ftplib.test() + self.assertEqual(cm.exception.code, 0) + output = stdout.getvalue() + self.assertIn('Usage:', output) + self.assertIn('Options:', output) + self.assertIn('Commands:', output) + + @requires_subprocess + def test_subprocess_invocation_no_args(self): + from test.support.script_helper import assert_python_ok + rc, out, err = assert_python_ok('-m', 'ftplib') + self.assertEqual(rc, 0) + self.assertIn(b'Usage:', out) + self.assertIn(b'Options:', out) + self.assertIn(b'Commands:', out) + self.assertEqual(err, b'') + + @mock.patch('ftplib.FTP') + def test_cli_host_and_debugging(self, mock_ftp_cls): + mock_ftp = mock_ftp_cls.return_value + with mock.patch('sys.argv', ['ftplib', '-d', '-d', 'example.com']): + ftplib.test() + mock_ftp_cls.assert_called_once_with('example.com') + mock_ftp.set_debuglevel.assert_called_once_with(2) + mock_ftp.login.assert_called_once_with('', '', '') + mock_ftp.quit.assert_called_once() + + @mock.patch('ftplib.FTP') + def test_cli_commands(self, mock_ftp_cls): + mock_ftp = mock_ftp_cls.return_value + with mock.patch('sys.argv', ['ftplib', 'example.com', '-l/pub', '-d/incoming', '-p']): + ftplib.test() + mock_ftp_cls.assert_called_once_with('example.com') + mock_ftp.dir.assert_called_once_with('/pub') + mock_ftp.sendcmd.assert_called_once_with('CWD /incoming') + mock_ftp.set_pasv.assert_called_once_with(not mock_ftp.passiveserver) + mock_ftp.quit.assert_called_once() + + @mock.patch('ftplib.FTP') + def test_cli_retrieve_file(self, mock_ftp_cls): + mock_ftp = mock_ftp_cls.return_value + with mock.patch('sys.argv', ['ftplib', 'example.com', 'testfile.txt']): + ftplib.test() + mock_ftp.retrbinary.assert_called_once() + args, kwargs = mock_ftp.retrbinary.call_args + self.assertEqual(args[0], 'RETR testfile.txt') + mock_ftp.quit.assert_called_once() + + @mock.patch('ftplib.FTP') + @mock.patch('netrc.netrc', side_effect=OSError('file not found')) + def test_cli_custom_rcfile_error(self, mock_netrc, mock_ftp_cls): + with mock.patch('sys.argv', ['ftplib', '-rnonexistent.netrc', 'example.com']), \ + support.captured_stderr() as stderr: + ftplib.test() + self.assertIn("Could not open account file -- using anonymous login.", + stderr.getvalue()) + + def setUpModule(): thread_info = threading_helper.threading_setup() unittest.addModuleCleanup(threading_helper.threading_cleanup, *thread_info) diff --git a/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst b/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst new file mode 100644 index 000000000000000..c01b89880132a38 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst @@ -0,0 +1 @@ +Add tests for ``ftplib`` command-line interface. From 2f1b6469e74eb255f691bdaec18d01c2c93f935e Mon Sep 17 00:00:00 2001 From: rudy-07 Date: Sun, 1 Nov 2026 20:37:14 +0530 Subject: [PATCH 2/3] gh-131178: Fix requires_subprocess decorator syntax and isolate netrc in ftplib CLI tests --- Lib/test/test_ftplib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index e8592969f7ef373..0467c3c782b61da 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -1205,7 +1205,7 @@ def test_main_no_arguments_prints_usage(self): self.assertIn('Options:', output) self.assertIn('Commands:', output) - @requires_subprocess + @requires_subprocess() def test_subprocess_invocation_no_args(self): from test.support.script_helper import assert_python_ok rc, out, err = assert_python_ok('-m', 'ftplib') @@ -1216,7 +1216,8 @@ def test_subprocess_invocation_no_args(self): self.assertEqual(err, b'') @mock.patch('ftplib.FTP') - def test_cli_host_and_debugging(self, mock_ftp_cls): + @mock.patch('netrc.netrc', side_effect=OSError) + def test_cli_host_and_debugging(self, mock_netrc, mock_ftp_cls): mock_ftp = mock_ftp_cls.return_value with mock.patch('sys.argv', ['ftplib', '-d', '-d', 'example.com']): ftplib.test() @@ -1226,18 +1227,20 @@ def test_cli_host_and_debugging(self, mock_ftp_cls): mock_ftp.quit.assert_called_once() @mock.patch('ftplib.FTP') - def test_cli_commands(self, mock_ftp_cls): + @mock.patch('netrc.netrc', side_effect=OSError) + def test_cli_commands(self, mock_netrc, mock_ftp_cls): mock_ftp = mock_ftp_cls.return_value with mock.patch('sys.argv', ['ftplib', 'example.com', '-l/pub', '-d/incoming', '-p']): ftplib.test() mock_ftp_cls.assert_called_once_with('example.com') mock_ftp.dir.assert_called_once_with('/pub') mock_ftp.sendcmd.assert_called_once_with('CWD /incoming') - mock_ftp.set_pasv.assert_called_once_with(not mock_ftp.passiveserver) + mock_ftp.set_pasv.assert_called_once() mock_ftp.quit.assert_called_once() @mock.patch('ftplib.FTP') - def test_cli_retrieve_file(self, mock_ftp_cls): + @mock.patch('netrc.netrc', side_effect=OSError) + def test_cli_retrieve_file(self, mock_netrc, mock_ftp_cls): mock_ftp = mock_ftp_cls.return_value with mock.patch('sys.argv', ['ftplib', 'example.com', 'testfile.txt']): ftplib.test() From 35c8cfc63c53e8fbc674ffd50eb2a2b56820e989 Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Sun, 1 Nov 2026 11:34:52 -0800 Subject: [PATCH 3/3] Delete Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst Don't need this for the test only enhancement. --- .../next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst diff --git a/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst b/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst deleted file mode 100644 index c01b89880132a38..000000000000000 --- a/Misc/NEWS.d/next/Tests/2026-11-01-19-45-00.gh-issue-131178.ftplib.rst +++ /dev/null @@ -1 +0,0 @@ -Add tests for ``ftplib`` command-line interface.