Repository: pypa/pip

Issue:
Cannot `pip install` SVN dependency with authentication using SVN 1.8+
**Environment**

* pip version: 19.0.3
* Python version: 3.6.7
* OS: Ubuntu 18.04 x64 LTS

<!-- Feel free to add more information about your environment here -->

**Description**

> [`pip` Subversion docs](https://pip.pypa.io/en/stable/reference/pip_install/#subversion)

Starting with SVN 1.8, [SVN is non-interactive by default](http://svn.apache.org/viewvc?view=revision&revision=1424037). Before that, it will prompt for a password when the user performs a `svn checkout`.

The problem is that when `pip` calls out to `svn checkout` it is not interactive, and will not allow the user to enter their password. One solution is to store SVN passwords, but that may not be allowed by company rules or that may simply not be desirable for security reasons.

For some more context see

- https://github.com/gedex/wp-tools/issues/33
- https://groups.google.com/forum/#!topic/subversion_users/fQ4K5GShGPU
- https://stackoverflow.com/questions/26066979

The solution seems to be:

1. If SVN version is `<1.8`, work as it does now (no extra arguments needed).
2. If SVN version is `>=1.8`, add the `--force-interactive` command line flag.

Some context from `svn checkout --help`

```
  --non-interactive        : do no interactive prompting (default is to prompt
                             only if standard input is a terminal device)
  --force-interactive      : do interactive prompting even if standard input
                             is not a terminal device
```

Perhaps another solution would be to make `svn` think it's being called from a terminal device when called from `pip`?

[Using environment variables is another potential option](https://pip.pypa.io/en/stable/reference/pip_install/#id10), but it runs into the same fundamental issue: users have to store their password (and in this case in plaintext). This doesn't seem to be appropriate for user workstations that could be shared.

Subversion versions on popular supported Linux distros:

- Ubuntu 18.04 LTS: 1.9.7
- Ubuntu 16.04 LTS: 1.9.3
- RHEL 7 / CentOS 7: 1.7.14
- RHEL 6 / CentOS 6: 1.6.11

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->

On SVN 1.7, 1.8, and 1.9, when `pip` installs an SVN dependency, it prompts the user for their password if they have not saved it locally.

**How to Reproduce**
<!-- Describe the steps to reproduce this bug. -->

1. Install Subversion >=1.8. 

  - If using Ubuntu 18.04, `sudo apt install subversion`. `svn --version` will return `1.9.7`.

2. Make a `venv`:

```bash
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install pip==19.0.3
```

3. Don't save your SVN password and try to `pip install` an SVN URL from a URL that requires authentication. `pip`'s invocation of `svn` will not prompt for a password, so it will always fail to install.

```bash
(venv) $ pip install svn+https://my-svn-server.com/project
Collecting svn+https://my-svn-server.com/project
   Checking out https://my-svn-server.com/project /tmp/pip-req-build-1g_qavb
svn: E170013: Unable to connect to repository at URL 'https://https://my-svn-server.com/project'
svn: E215004: No more credentials or we tried too many times.
Authentication failed.
Command "svn checkout -q https://my-svn-server.com/project /tmp/pip-req-build-1g_qavb" failed with error code 1 in None
```

**References**

- https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/vcs/subversion.py#L58

- https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/vcs/__init__.py#L546
- https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/utils/misc.py#L722


Discussion:
It seems like the most "proper" way would be to make [`Popen`](https://docs.python.org/3/library/subprocess.html) calls to SVN with a psuedo tty, so that SVN behaves as if it were called from the terminal directly.

- [`pty`](https://docs.python.org/3/library/pty.html)
- https://gist.github.com/thomasballinger/7979808
- https://stackoverflow.com/a/43012138/1398841
There does seem to be precedent for special casing based on Subversion version:

https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/vcs/subversion.py#L157

https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/vcs/subversion.py#L44

https://github.com/pypa/pip/blob/e5353f27cdc48494018c9b4eee94a44573eceeed/src/pip/_internal/vcs/subversion.py#L174-L175
My initial thought on this is that passing `--force-interactive` would be better than trying to use a pseudo-terminal.

Also, it seems like this shouldn't be done by default, but perhaps only if we can be sure pip is being used interactively (e.g. if `sys.stdout.isatty()` returns `True`) -- a bit like how `svn` is described as behaving.

It's worth pointing out that ptys aren't an option on Windows.
@cjerdonek Yeah, I agree that a pseudo-terminal would be a more complex solution.

The only issue with trying to pass `--force-interactive` regardless of SVN version would be that SVN 1.7 and 1.6 don't come with this option, so doing that would break compatibility with RHEL/CentOS 6 and RHEL/CentOS 7. (Gotta love ancient supported toolchains...)

We could work around this with `svn --version`, but I don't know if that is a bridge too far for a tool like `pip`.

I *think* this could be a proposed solution (in pseudocode):

```py
svn_version = get_version()  # `svn --version`
if svn_version >= 1.8 and sys.stdout.isatty():
    svn_options.append('--force-interactive')
else:
    # Do nothing
    pass
```

Thoughts?
While this might look easy, I think this might take a bit more work in certain ways to fully get right. Some thoughts:

1. If pip will be in a mode expecting interaction, we'd want to be sure that the output to the user (e.g. command prompts) is flushed as it is currently going through logging code (and also accept stdin). This will add additional complications to `call_subprocess()`.
2. If we need to call out to `svn` to get the version, I think we'd only want to do that once (lazily), and then cache the result. (The `Subversion` class isn't structured in the way that I'd personally like for this yet, but it's close.)
3. To do this properly, we would probably want to do what SVN is doing and provide command-line options to force the behavior one way or the other (e.g. if the user is redirecting stdout but still wants interactivity). Similar to (2), the `VersionControl` class (and subclasses) isn't really set up yet to accept command-line options.
4. Lastly, I don't think we'd need to worry about providing new functionality like this for older versions of SVN.

> Lastly, I don't think we'd need to worry about providing new functionality like this for older versions of SVN.

Okay, so just so I'm clear, you mean not supporting Subversion 1.6 and 1.7 (RHEL 6/7 and CentOS 6/7)? So we would push the `--force-interactive` flag without regard for what version of Subversion they are running (since 1.6/1.7 don't have the flag?

```bash
# CentOS 7
$ svn --version
svn, version 1.7.14 (r1542130)
   compiled Apr 11 2018, 02:40:28

$ svn --force-interactive
svn: invalid option: --force-interactive
```

I don't really disagree in principle (it's a burden to support old tools), but just wanted to be clear that `pip` would then no longer be able to work with the platform installed Subversion on RHEL 6/7 and CentOS 6/7. [RHEL/CentOS 7 will be supported until 2024](https://en.wikipedia.org/wiki/CentOS#End-of-support_schedule) 😕.

> (The `Subversion` class isn't structured in the way that I'd personally like for this yet, but it's close.)

If we can agree on a way forward I'm willing to try to put together a PR. But if it involves refactoring the `Subversion` class, a `pip` maintainer would probably be better suited. If a `pip` maintainer made the refactorings you suggest, I would be willing give it a shot to add `--force-interactive` on top when needed.
> So we would push the --force-interactive flag without regard for what version of Subversion they are running (since 1.6/1.7 don't have the flag?

I meant passing the flag only for SVN 1.8+ (which is why I also suggested caching the value of getting the version). But sorry, I had missed or forgotten that older versions were interactive by default in the absence of any option. So you can disregard that last bullet. :)

> I would be willing give it a shot to add --force-interactive on top when needed.

Okay, great. But yeah, I'd prefer if you waited a bit, if that's okay. I actually have a VCS-related PR queued up (as a follow-up to PR #6356) that I'm now planning to modify in response to this issue. (The PR should become simpler in fact, and should help for this.) I'd also like to think about how `call_subprocess()` should best be changed for this, because the logging there was just reworked to be simpler, and I'd like to avoid re-introducing too much more complexity there, if possible.

If you want I can describe some of the VCS changes I'd like to see. They aren't super hard but may involve touching a few things.

I'll also think to see what, if any, of this PR can be done in advance of the refactorings.

One thing you could do now in preparation is add a `get_vcs_version()` class method to the `Subversion` class that returns a tuple, and add a test for it. There's already a `Git.get_git_version()` method you could look at for ideas. The test could be marked to run only if SVN is installed, and at least one of the test environments should require that SVN be installed. (You can look at the last Bazaar PR to see how that was done.)
Btw, is there a way to prevent interactivity on older versions of SVN (e.g. to prevent hangs)?
> Btw, is there a way to prevent interactivity on older versions of SVN (e.g. to prevent hangs)?

```bash
# CentOS 7.6.
$ svn --version
svn, version 1.7.14 (r1542130)
   compiled Apr 11 2018, 02:40:28
$ svn checkout --help
...
Global options:
  --username ARG           : specify a username ARG
  --password ARG           : specify a password ARG
  --no-auth-cache          : do not cache authentication tokens
  --non-interactive        : do no interactive prompting
```

`--non-interactive` should be what you want, and it is available at least in SVN 1.7.
@cjerdonek I submitted #6390, let me know if that is what you were looking for.
> --non-interactive should be what you want, and it is available at least in SVN 1.7.

Thanks. A related positive change IMO would be to pass that flag to SVN when SVN is 1.7 and `isatty()` returns `False` (the flip-side of the case you're interested in, to prevent hangs when pip isn't being used interactively).

>  A related positive change IMO would be to pass that flag to SVN when SVN is 1.7 and isatty() returns False

@cjerdonek I think we need to be very cautious about this. For example, it's currently very useful that when `pip` is called from `tox` on CentOS 7 with SVN 1.7, that `svn` prompts for a password. I doubt `pip` is in a tty at that point.

`tox` -> `pip` -> `svn`: prompt user for password.

This allows us to use `tox` from the terminal against private SVN repos, but not have users store their passwords on the system.

It's not a difficult configuration to make sure `svn` *only* prompts you when you *want* it to prompt you (e.g. for a password you are intending not to store).
> For example, it's currently very useful that when pip is called from tox on CentOS 7 with SVN 1.7, that svn prompts for a password. I doubt pip is in a tty at that point.

Okay, good to know. This is also why I said the following above:

> 3. To do this properly, we would probably want to do what SVN is doing and provide command-line options to force the behavior one way or the other (e.g. if the user is redirecting stdout but still wants interactivity).

By the way, your point would also apply to the SVN 1.8+ case, with the approach for this PR of checking `isatty()`, right? 
> For example, it's currently very useful that when pip is called from tox on CentOS 7 with SVN 1.7, that svn prompts for a password.

Actually, can you confirm whether you can really do this with recent versions of pip? It appears as if this isn't possible as of pip 10.0 (since `stdin` is closed after the call to `subprocess.Popen()`): https://github.com/pypa/pip/pull/4982
> By the way, your point would also apply to the SVN 1.8+ case, with the approach for this PR of checking isatty(), right?

Yeah, I'm wondering if we should avoid suppressing interactive in either case based on `isatty()`. It's a tough one, because on one hand getting `svn` to ignore superfluous prompts is nice, but if it makes it so it's not possible to input a password at all when run from a tool like `tox`, that is a problem. I'm not sure how `tox` calls `pip`, to know whether it thinks its in a `tty` or not, but we should look into it when fixing this problem.

> Actually, can you confirm whether you can really do this with recent versions of pip? It appears as if this isn't possible as of pip 10.0 (since stdin is closed after the call to subprocess.Popen()): #4982

I can confirm the following configurations all work with stock installed tools on CentOS 7.

- Python 3.6.6 from EPEL
- `svn` 1.7.14 from the base CentOS 7 repo
- `pip` 19.0.3
- `tox` 3.7.0

## Configuration

`~/.subversion/servers` has the following option set so that `svn` does not prompt the user whether they want to store passwords (which would break `pip` usage).

```
store-plaintext-passwords = no
```

## Test 1: `pip` from terminal

```bash
$ pip install svn+https://my-private-svn.com/svn/project/project-1

# Prompts for password, user enters
# pip successfully installs `project-1`
```

## Test 2: `pip` from terminal with `requirements.txt`

`requirements.txt`:
```
svn+https://my-private-svn.com/svn/project/project-1
```

```bash
$ pip install -r requirements.txt

# Prompts for password, user enters
# pip successfully installs `project-1`
```

## Test 3: `tox`

`requirements.txt`:
```
svn+https://my-private-svn.com/svn/project/project-1
```

`tox.ini`:

```ini
...
[testenv]
deps =
    -r{toxinidir}/requirements.txt
...
```

```bash
$ tox --recreate
py36 create: ...
py36 installdeps -r /home/user/requirements.txt
# Prompts for password, user enters
# pip successfully installs `project-1`
...
```

## Follow Up

I don't know the intricacies of Linux terminals well, but it would appear that `stdin` prompts are different in some ways than these password prompts? For one, you don't see what you type when you enter passwords, so is there some other password/authentication-specific Linux command going on here?
@cjerdonek After some digging into the Subversion source code (always a fun activity), I found where `svn` is opening the special device node `/dev/tty`:

https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_subr/prompt.c

As I understand it, `/dev/tty` is a special device node that refers to the controlling terminal for a process. This allows `svn` to prompt for a password directly to the terminal regardless of what other processes have called it.

So I believe this explains why `stdin` doesn't apply in the case of password prompts from `svn`.
After some more testing, perhaps the `tox` case not making `pip` think it's in a `tty` is not actually a concern.

I wrote a small Python script that checked if `stdout`, `stderr`, and `stdin` `.isatty()`.

```ini
# tox.ini
[testenv]
commands = python tty_test.py
```

```bash
$ tox -e py36
```
All three returned `True`. This doesn't 100% prove that when `pip` is called (it is used in the `deps` section of `tox.ini`), it thinks it's in a `tty`, but gives me hope that this may not be an issue.
Thanks for all your research and digging! 👍 
@johnthagen Let me know if you want my thoughts on the next steps, or if you want to propose something and I can react to that. I think it might be helpful to do at least one more incremental PR before landing the functionality.
For reference, `svn` options are:

```
# svn <= 1.7
 --non-interactive        : do no interactive prompting
```

```
# svn >= 1.8
  --non-interactive        : do no interactive prompting (default is to prompt
                             only if standard input is a terminal device)
  --force-interactive      : do interactive prompting even if standard input
                             is not a terminal device
```

Just so we're on the same page, our current pseudocode proposal is:

```python
svn_version = get_vcs_version()  # `svn --version`
if not sys.stdin.isatty():
    # TODO: Test that `tox` `deps` install step runs `pip` in a TTY.
    svn_options.append('--non-interactive')
elif svn_version >= 1.8:
    svn_options.append('--force-interactive')
```

Or a more conservative proposal is to just address needing to get the password prompt on 1.8+.

```python
svn_version = get_vcs_version()  # `svn --version`
if svn_version >= 1.8 and sys.stdin.isatty():
    svn_options.append('--force-interactive')
```

The second has less risk of breaking situations were you want to be prompted on 1.7, but has the risk that 1.7 and 1.8 could behave differently from `pip`. The first approach is probably better (I just wish I had a better handle on *when* `isatty()` will be `False`).

What are your thoughts?
Do you know for sure that `--non-interactive` is present in all SVN versions <= 1.7?

I was thinking the next PR could define and test a method on the `Subversion` class that returns the list of global options to use (empty list, `['--non-interactive']`, or `['--force-interactive']`), but not start including them in the actual invocations yet. (Do you know which SVN invocations these options will need to be passed to, by the way?)

I think the `Subversion` class's `__init__()` method should start accepting an optional `use_interactive` argument (or some other name) that defaults to a value based on `sys.stdin.isatty()`. This will help with unit-testing the method above. It can also be used later to force certain behavior e.g. via pip command-line options.

I also think the class should grow a private `_vcs_version` instance attribute to cache the version value from the method you just wrote. It can initially be set to `None` (unset), with a `None` version value being stored as some other non-`None` value (perhaps `False`). The `get_vcs_version()` method can be changed to check this instance attribute, and call a class method called something like `call_vcs_version()` that would contain your current logic. This private instance attribute will also help with unit-testing the method that returns the interactive options, because it will make it easy to test different "versions" of SVN.

I agree with your summary, by the way. Thanks!
> Do you know for sure that --non-interactive is present in all SVN versions <= 1.7?

According to: https://svn.apache.org/repos/asf/subversion/trunk/CHANGES

> Version 0.14.4 [Alpha Interim 4] (released 29 Oct 2002, revision 3553)
>  * new --non-interactive switch for commandline client

> (Do you know which SVN invocations these options will need to be passed to, by the way?)

I believe this is where the options would need to be applied:

https://github.com/pypa/pip/blob/c8e9caa8a717ea3e43dffa91b07f7cfd79e057a0/src/pip/_internal/vcs/subversion.py#L84-L93

`pip` checks out the Subversion project to a folder in `/tmp` before installing it from that folder.
Regarding the various SVN invocations to consider, there are also invocations of commands like "export", "update", etc. When editable mode is being used, a different directory is used, and in-place updates can occur. I'm guessing at least "update" would also need the options passed.

Re: tox, I did a quick check of the tox code base, and it looks like `stdin` is never passed to its subprocess calls. Perhaps this means any TTY is preserved (unlike pip which passes `stdin=subprocess.PIPE`).
Can confirm the following commands are using in `subversion.py` and support `--non-interactive` and `--force-interactive`:

- `export`
- `checkout`
- `switch`
- `update`
- `info`
@cjerdonek With #6439 merged, I *think* the last thing that needs to be completed is for us to actually add the `get_remote_call_options()` result to the invocations of `svn` where they are needed:

https://github.com/pypa/pip/blob/07ce2ab2b76ca3f66ab109910f2668163f4779da/src/pip/_internal/vcs/subversion.py#L271-L278

Since this is when we actually change functionality, I plan to additionally test out the proposed `pip` egg with this change on some private SVN repos (on SVN 1.7 & 1.8+) to make sure everything is working as we expect before we merge the PR.

How would you like to proceed?
@johnthagen That sounds good (and glad to hear you can test out on private repos). I don't _think_ there will be much code. But if you have any questions in advance, feel free to let me know.

Resolving patch:
diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py
--- a/src/pip/_internal/vcs/subversion.py
+++ b/src/pip/_internal/vcs/subversion.py
@@ -20,6 +20,7 @@
 
 if MYPY_CHECK_RUNNING:
     from typing import List, Optional, Tuple
+    from pip._internal.vcs import RevOptions
 
 logger = logging.getLogger(__name__)
 
@@ -48,10 +49,12 @@ def export(self, location, url):
                 # Subversion doesn't like to check out over an existing
                 # directory --force fixes this, but was only added in svn 1.5
                 rmtree(location)
-            cmd_args = ['export'] + rev_options.to_args() + [url, location]
+            cmd_args = (['export'] + self.get_remote_call_options() +
+                        rev_options.to_args() + [url, location])
             self.run_command(cmd_args, show_stdout=False)
 
     def fetch_new(self, dest, url, rev_options):
+        # type: (str, str, RevOptions) -> None
         rev_display = rev_options.to_display()
         logger.info(
             'Checking out %s%s to %s',
@@ -59,15 +62,21 @@ def fetch_new(self, dest, url, rev_options):
             rev_display,
             display_path(dest),
         )
-        cmd_args = ['checkout', '-q'] + rev_options.to_args() + [url, dest]
+        cmd_args = (['checkout', '-q'] +
+                    self.get_remote_call_options() +
+                    rev_options.to_args() + [url, dest])
         self.run_command(cmd_args)
 
     def switch(self, dest, url, rev_options):
-        cmd_args = ['switch'] + rev_options.to_args() + [url, dest]
+        # type: (str, str, RevOptions) -> None
+        cmd_args = (['switch'] + self.get_remote_call_options() +
+                    rev_options.to_args() + [url, dest])
         self.run_command(cmd_args)
 
     def update(self, dest, url, rev_options):
-        cmd_args = ['update'] + rev_options.to_args() + [dest]
+        # type: (str, str, RevOptions) -> None
+        cmd_args = (['update'] + self.get_remote_call_options() +
+                    rev_options.to_args() + [dest])
         self.run_command(cmd_args)
 
     @classmethod
@@ -177,6 +186,11 @@ def _get_svn_url_rev(cls, location):
         else:
             try:
                 # subversion >= 1.7
+                # Note that using get_remote_call_options is not necessary here
+                # because `svn info` is being run against a local directory.
+                # We don't need to worry about making sure interactive mode
+                # is being used to prompt for passwords, because passwords
+                # are only potentially needed for remote server requests.
                 xml = cls.run_command(
                     ['info', '--xml', location],
                     show_stdout=False,


Test patch:
diff --git a/tests/functional/test_install_vcs_svn.py b/tests/functional/test_install_vcs_svn.py
--- a/tests/functional/test_install_vcs_svn.py
+++ b/tests/functional/test_install_vcs_svn.py
@@ -1,30 +1,66 @@
-import pytest
+from unittest import TestCase
+
 from mock import patch
 
+from pip._internal.vcs import RevOptions
 from pip._internal.vcs.subversion import Subversion
 
 
-@patch('pip._internal.vcs.call_subprocess')
-@pytest.mark.network
-def test_obtain_should_recognize_auth_info_url(call_subprocess_mock, script):
-    url = 'svn+http://username:password@svn.example.com/'
-    svn = Subversion()
-    svn.obtain(script.scratch_path / 'test', url=url)
-    assert call_subprocess_mock.call_args[0][0] == [
-        svn.name, 'checkout', '-q', '--username', 'username', '--password',
-        'password', 'http://svn.example.com/',
-        script.scratch_path / 'test',
-    ]
-
-
-@patch('pip._internal.vcs.call_subprocess')
-@pytest.mark.network
-def test_export_should_recognize_auth_info_url(call_subprocess_mock, script):
-    url = 'svn+http://username:password@svn.example.com/'
-    svn = Subversion()
-    svn.export(script.scratch_path / 'test', url=url)
-    assert call_subprocess_mock.call_args[0][0] == [
-        svn.name, 'export', '--username', 'username', '--password',
-        'password', 'http://svn.example.com/',
-        script.scratch_path / 'test',
-    ]
+class TestSubversionArgs(TestCase):
+    def setUp(self):
+        patcher = patch('pip._internal.vcs.call_subprocess')
+        self.addCleanup(patcher.stop)
+        self.call_subprocess_mock = patcher.start()
+
+        # Test Data.
+        self.url = 'svn+http://username:password@svn.example.com/'
+        # use_interactive is set to False to test that remote call options are
+        # properly added.
+        self.svn = Subversion(use_interactive=False)
+        self.rev_options = RevOptions(Subversion)
+        self.dest = '/tmp/test'
+
+    def assert_call_args(self, args):
+        assert self.call_subprocess_mock.call_args[0][0] == args
+
+    def test_obtain(self):
+        self.svn.obtain(self.dest, self.url)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive', '--username',
+             'username', '--password', 'password',
+             'http://svn.example.com/', '/tmp/test'])
+
+    def test_export(self):
+        self.svn.export(self.dest, self.url)
+        self.assert_call_args(
+            ['svn', 'export', '--non-interactive', '--username', 'username',
+             '--password', 'password', 'http://svn.example.com/',
+             '/tmp/test'])
+
+    def test_fetch_new(self):
+        self.svn.fetch_new(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_fetch_new_revision(self):
+        rev_options = RevOptions(Subversion, '123')
+        self.svn.fetch_new(self.dest, self.url, rev_options)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive',
+             '-r', '123',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_switch(self):
+        self.svn.switch(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'switch', '--non-interactive',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_update(self):
+        self.svn.update(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'update', '--non-interactive', '/tmp/test'])