|
1 | 1 | import platform |
2 | 2 | import tempfile |
3 | 3 | import time |
| 4 | +from unittest.mock import patch |
4 | 5 |
|
5 | 6 | import pytest |
6 | 7 |
|
7 | 8 | import click._termui_impl |
8 | 9 | from click._compat import WIN |
| 10 | +from click._termui_impl import Editor |
9 | 11 | from click.exceptions import BadParameter |
10 | 12 | from click.exceptions import MissingParameter |
11 | 13 |
|
@@ -404,6 +406,120 @@ def test_edit(runner): |
404 | 406 | assert reopened_file.read() == "aTest\nbTest\n" |
405 | 407 |
|
406 | 408 |
|
| 409 | +@pytest.mark.parametrize( |
| 410 | + ("editor_cmd", "filenames", "expected_args"), |
| 411 | + [ |
| 412 | + pytest.param( |
| 413 | + "myeditor --wait --flag", |
| 414 | + ["file1.txt", "file2.txt"], |
| 415 | + ["myeditor", "--wait", "--flag", "file1.txt", "file2.txt"], |
| 416 | + id="editor with args", |
| 417 | + ), |
| 418 | + pytest.param( |
| 419 | + "vi", |
| 420 | + ['file"; rm -rf / ; echo "'], |
| 421 | + ["vi", 'file"; rm -rf / ; echo "'], |
| 422 | + id="shell metacharacters in filename", |
| 423 | + ), |
| 424 | + # Issue #1026: editor path with spaces must be quoted. |
| 425 | + pytest.param( |
| 426 | + '"C:\\Program Files\\Sublime Text 3\\sublime_text.exe"', |
| 427 | + ["f.txt"], |
| 428 | + ["C:\\Program Files\\Sublime Text 3\\sublime_text.exe", "f.txt"], |
| 429 | + id="quoted windows path with spaces (issue 1026)", |
| 430 | + ), |
| 431 | + # PR #1477: pager/editor command with flags, like ``less -FRSX``. |
| 432 | + pytest.param( |
| 433 | + "less -FRSX", |
| 434 | + ["f.txt"], |
| 435 | + ["less", "-FRSX", "f.txt"], |
| 436 | + id="command with flags (pr 1477)", |
| 437 | + ), |
| 438 | + # Issue #1026: quoted command with ``--wait`` flag. |
| 439 | + pytest.param( |
| 440 | + '"my command" --option value arg', |
| 441 | + ["f.txt"], |
| 442 | + ["my command", "--option", "value", "arg", "f.txt"], |
| 443 | + id="quoted command with args (issue 1026)", |
| 444 | + ), |
| 445 | + # PR #1477: unquoted unix path. |
| 446 | + pytest.param( |
| 447 | + "/usr/bin/vim", |
| 448 | + ["f.txt"], |
| 449 | + ["/usr/bin/vim", "f.txt"], |
| 450 | + id="unix absolute path", |
| 451 | + ), |
| 452 | + # Issue #1026: macOS path with escaped space. |
| 453 | + pytest.param( |
| 454 | + "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl", |
| 455 | + ["f.txt"], |
| 456 | + ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "f.txt"], |
| 457 | + id="escaped space in unix path (issue 1026)", |
| 458 | + ), |
| 459 | + ], |
| 460 | +) |
| 461 | +def test_editor_path_normalization(editor_cmd, filenames, expected_args): |
| 462 | + with patch("subprocess.Popen") as mock_popen: |
| 463 | + mock_popen.return_value.wait.return_value = 0 |
| 464 | + Editor(editor=editor_cmd).edit_files(filenames) |
| 465 | + |
| 466 | + mock_popen.assert_called_once() |
| 467 | + args = mock_popen.call_args[1].get("args") or mock_popen.call_args[0][0] |
| 468 | + assert args == expected_args |
| 469 | + assert mock_popen.call_args[1].get("shell") is None |
| 470 | + |
| 471 | + |
| 472 | +@pytest.mark.skipif(not WIN, reason="Windows-specific editor paths") |
| 473 | +@pytest.mark.parametrize( |
| 474 | + ("editor_cmd", "expected_cmd"), |
| 475 | + [ |
| 476 | + pytest.param( |
| 477 | + "notepad", |
| 478 | + ["notepad"], |
| 479 | + id="plain notepad", |
| 480 | + ), |
| 481 | + pytest.param( |
| 482 | + '"C:\\Program Files\\Sublime Text 3\\sublime_text.exe" --wait', |
| 483 | + ["C:\\Program Files\\Sublime Text 3\\sublime_text.exe", "--wait"], |
| 484 | + id="quoted path with flag", |
| 485 | + ), |
| 486 | + ], |
| 487 | +) |
| 488 | +def test_editor_windows_path_normalization(editor_cmd, expected_cmd): |
| 489 | + """Windows-specific tests: verify ``Popen`` receives unquoted paths that |
| 490 | + ``subprocess.list2cmdline`` can re-quote for ``CreateProcess``.""" |
| 491 | + with patch("subprocess.Popen") as mock_popen: |
| 492 | + mock_popen.return_value.wait.return_value = 0 |
| 493 | + Editor(editor=editor_cmd).edit_files(["f.txt"]) |
| 494 | + |
| 495 | + args = mock_popen.call_args[1].get("args") or mock_popen.call_args[0][0] |
| 496 | + assert args == expected_cmd + ["f.txt"] |
| 497 | + assert mock_popen.call_args[1].get("shell") is None |
| 498 | + |
| 499 | + |
| 500 | +def test_editor_env_passed_through(): |
| 501 | + with patch("subprocess.Popen") as mock_popen: |
| 502 | + mock_popen.return_value.wait.return_value = 0 |
| 503 | + Editor(editor="vi", env={"MY_VAR": "1"}).edit_files(["f.txt"]) |
| 504 | + |
| 505 | + env = mock_popen.call_args[1].get("env") |
| 506 | + assert env is not None |
| 507 | + assert env["MY_VAR"] == "1" |
| 508 | + |
| 509 | + |
| 510 | +def test_editor_failure_exception(): |
| 511 | + with patch("subprocess.Popen") as mock_popen: |
| 512 | + mock_popen.return_value.wait.return_value = 1 |
| 513 | + with pytest.raises(click.ClickException, match="Editing failed"): |
| 514 | + Editor(editor="vi").edit_files(["f.txt"]) |
| 515 | + |
| 516 | + |
| 517 | +def test_editor_nonexistent_exception(): |
| 518 | + with patch("subprocess.Popen", side_effect=OSError("not found")): |
| 519 | + with pytest.raises(click.ClickException, match="not found"): |
| 520 | + Editor(editor="nonexistent").edit_files(["f.txt"]) |
| 521 | + |
| 522 | + |
407 | 523 | @pytest.mark.parametrize( |
408 | 524 | ("prompt_required", "required", "args", "expect"), |
409 | 525 | [ |
|
0 commit comments