From 21bff19093a616f65af9eaa3fd026ee09ada8242 Mon Sep 17 00:00:00 2001 From: Rudransh Shekhar Date: Mon, 2 Nov 2026 01:36:55 +0530 Subject: [PATCH 1/2] gh-131178: Add tests for `ftplib` command-line interface (GH-161080) * 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. (cherry picked from commit c29f95a98310fb0a8a7d432741d4753872a57d0f) Co-authored-by: Rudransh Shekhar Co-authored-by: Senthil Kumaran --- Lib/test/test_ftplib.py | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index f1eff9430f7351c..30ec6fbb3dd82af 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -1188,6 +1188,74 @@ 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') + @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() + 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') + @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() + mock_ftp.quit.assert_called_once() + + @mock.patch('ftplib.FTP') + @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() + 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) From 9f8dcb2b686d496286fba34b752aabba43651141 Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Mon, 2 Nov 2026 00:11:32 -0800 Subject: [PATCH 2/2] Fix the buildbot. --- 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 30ec6fbb3dd82af..a7fc855c3833292 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -1239,15 +1239,18 @@ def test_cli_commands(self, mock_netrc, 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']): + with mock.patch('sys.argv', ['ftplib', 'example.com', 'testfile.txt']), \ + mock.patch('sys.stdout') as mock_stdout: ftplib.test() - mock_ftp.retrbinary.assert_called_once() - args, kwargs = mock_ftp.retrbinary.call_args - self.assertEqual(args[0], 'RETR testfile.txt') + mock_ftp.retrbinary.assert_called_once_with( + 'RETR testfile.txt', mock_stdout.buffer.write, 1024 + ) + mock_stdout.buffer.flush.assert_called_once() + mock_stdout.flush.assert_called_once() mock_ftp.quit.assert_called_once() @mock.patch('ftplib.FTP') - @mock.patch('netrc.netrc', side_effect=OSError('file not found')) + @mock.patch('netrc.netrc', side_effect=OSError) 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: