Repository: pypa/virtualenv

Issue:
[1168/1285] Implement tests for prompt behavior of activate.fish
Per discussion at #1168 and #1285, as part of harmonizing the prompt handling behavior of the various supported shells, implement tests to confirm the following behaviors of `activate.fish`:

 * Obeys `VIRTUAL_ENV_DISABLE_PROMPT`
 * Implements default `__VIRTUAL_PROMPT__`, prompt prefix of "`($VIRTUAL_ENV) `", when `--prompt` is not supplied
 * Creates `__VIRTUAL_PROMPT__` prompt prefix from argument to `--prompt` *without* inserting a space
[1168/1285] Fix description of VIRTUAL_ENV_DISABLE_PROMPT behavior in reference guide
The [documentation for `$VIRTUAL_ENV_DISABLE_PROMPT`](https://virtualenv.pypa.io/en/latest/reference/#envvar-VIRTUAL_ENV_DISABLE_PROMPT) currently states that:

> Any virtualenv created when this is set to a non-empty value will not have it’s activate script modify the shell prompt.

This conflicts with both the current behavior [implemented in `activate.sh`](https://github.com/pypa/virtualenv/blob/23e7f7a628b6a3801ee4764ed6ffc7e71ff038bd/virtualenv_embedded/activate.sh#L56-L64) and that described in the [user's guide](https://virtualenv.pypa.io/en/latest/userguide/#activate), where a truthy value of `$VIRTUAL_ENV_DISABLE_PROMPT` has the effect of suppressing modification of the prompt **at the time of environment *activation***.

As part of #1285, the docs should be revised here to reflect the actual behavior.
COSMETIC: On Windows, --prompt adds a space to the resulting prompt after its argument; doesn't add space on Linux
When creating a new virtualenv with the `--prompt` option, on Windows a space is automatically added between the `--prompt=` value and the tail of the prompt:

```
C:\temp>virtualenv env --prompt="(myenv)"
...

C:\temp>env\scripts\activate

(myenv) C:\temp> 
```

On Debian stretch, a space is not added:

```
me@mycomp:~$ virtualenv env --prompt="(myenv)"
...

me@mycomp:~$ source env/bin/activate

(myenv)me@mycomp:~$ 
```

If auto-insertion of a space is desired behavior, it should be a simple fix: adding a space [here in `activate.sh`](https://github.com/pypa/virtualenv/blob/2f5c75bd4762463e904aa1c06ccba29dd817e940/virtualenv_embedded/activate.sh#L59), before the `$PS1`.

If this is desired, I can create a PR.

-----

**EDIT:** Per discussion below, the decision has been made to ***remove*** the mandatory space after the custom prompt string, for all shells.
ADMINISTRATIVE: CONTRIBUTING.rst & docstring of tasks/update_embedded.py appear out of date
[CONTRIBUTING.rst currently says](https://github.com/pypa/virtualenv/blob/59a5ab8ce460de950f97ffba8e1af6d79d530aa9/CONTRIBUTING.rst):

> * All changes to files inside `virtualenv_embedded` should be integrated to `virtualenv.py` with `bin/rebuild-script.py`.

If I read the project tree correctly, though, that update script is now [`tasks\update_embedded.py`](https://github.com/pypa/virtualenv/blob/59a5ab8ce460de950f97ffba8e1af6d79d530aa9/tasks/update_embedded.py).  As well, the script [docstring](https://github.com/pypa/virtualenv/blob/59a5ab8ce460de950f97ffba8e1af6d79d530aa9/tasks/update_embedded.py#L3) appears to be incorrect, as `virtualenv.py` is now updated from `virtualenv_embedded`, not `virtualenv_support`.

Small fixes, I'm happy to make the PR if I'm on the right track here.


Discussion:


@bskinn sure, feel free to do so.
maybe I'm being contrary, but I actually think the windows behaviour is incorrect here.  After adding a space it makes it impossible to produce a prompt _without_ a space.  However you can make a prompt with a space by doing `--prompt='(foo) '`
@asottile Very good point. At the time I composed this issue, I hadn't really tried to think of use-cases where a required space would be undesirable... but, for example, a forced space prevents crafting a prompt like:

```
envname-C:\git\project> 
```

For how ***I*** construct my prompts, I would like the space to be there by default.  But, you're right, it's far more flexible *not* to mandate the trailing space. I'm now inclined to go the other way with it, and remove the default space on Windows (~as long as `cmd` allows its removal...~ CONFIRMED, `cmd` does allow zero space).

@gaborbernat?
I can see peoples complaining once we remove, but let's bite the bullet. Let's remove it.
Ok, I will revise the PR to **REMOVE** the mandated space from both Windows [`cmd`](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.bat#L12) and [`bash`](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.sh#L59) templates, and add tests for both to check for the lack of the trailing space.

~It looks like a mandated space should similarly be removed from the [`csh`](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.csh#L34) and [PowerShell](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.ps1#L57) templates.  (If new tests are desired for these shells, I'm not sure how to add them so that they integrate with the CI.)~

OTOH, [`fish`](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.fish#L90) looks ok to me; and, if I read the [`xonsh`](https://github.com/pypa/virtualenv/blob/d683eaeb97895d90ba685dc2708774082d0e0e3f/virtualenv_embedded/activate.xsh) template correctly, it doesn't even mess with the prompt, and so doesn't need to be changed.
Just to raise the possibility...what if this change turns out to be a https://xkcd.com/1172/ ?  If, say, someone is inspecting %PROMPT% &c to sniff which venv is active.
Seems unlikely when $VIRTUAL_ENV is a thing
Now that I look at it, I think both `activate.csh` and `activate.ps1` actually ***ignore*** `--prompt`. The lines of code I linked to above appear actually just to always use the environment name as the default prefix.

Is this desired behavior? Or should these be revised to obey `--prompt`?
I would say should be revised 👍 
Indeed we need to upgrade those. Though we no longer recommend calling those scripts directly, but instead via tox (see embed, fix_lint targets). We would love you to make a PR fixing this, thanks!
Ok -- so, contributors should:

 1.  Run `tox -e embed` after making any changes to the scripts in `virtualenv_embedded`; and
 2.  Run `tox -e fix_lint` before committing to feature branches

?
yes 
Note to self .. temporarily setting `skip_install = False` in the `[testenv:fix_lint]` environment settings within `tox.ini` resolved a `.../.tox/fix_lint/bin/python: No module named virtualenv` error initially encountered. (**NOT to be committed back upstream!**)

Resolving patch:
diff --git a/virtualenv.py b/virtualenv.py
--- a/virtualenv.py
+++ b/virtualenv.py
@@ -1693,7 +1693,7 @@ def install_files(home_dir, bin_dir, prompt, files):
     virtualenv_name = os.path.basename(home_dir)
     for name, content in files.items():
         content = content.replace("__VIRTUAL_PROMPT__", prompt or "")
-        content = content.replace("__VIRTUAL_WINPROMPT__", prompt or "({})".format(virtualenv_name))
+        content = content.replace("__VIRTUAL_WINPROMPT__", prompt or "({}) ".format(virtualenv_name))
         content = content.replace("__VIRTUAL_ENV__", home_dir)
         content = content.replace("__VIRTUAL_NAME__", virtualenv_name)
         content = content.replace("__BIN_NAME__", os.path.basename(bin_dir))
@@ -2277,42 +2277,43 @@ def convert(s):
 # file activate.csh
 ACTIVATE_CSH = convert(
     """
-eJx9k9tq20AQhu/3Kf7IJm5N4vRarts6caCBxAnBCZSmLCtpXC1IK2e1svFNn72zklzkA9WFkOb0
-z34708Mi1SWWOiPkVekQEaqSEmy0SxGURWVjQqTNlYqdXitHo7hMAwyXtsjBn8OR6OFHUSFWxhQO
-tjLQDom2FLts6703ljgvQbTFTK11QphpXGeq1Pic1IYk+vY7VzobxUX+ZSRESQ6GNpk2NBm8iYEQ
-KtOqREK7LjBwxN32v8rH+5l8vXtevEzv5dN08R1nE3zC+Tm4CJk1alvQP4oL3wMfVRkvduQdw1Kq
-ynSMkzrPjw9Pi64SVsxj5SaHQnXgf6Rq/7hx+W53jtv5aysdvJ2Fw8BrBaYwCZts5SFQW/OITMe6
-2iZFzPR6eKm1tbWU0VoZh7WyWkUZlSPRyd1XqC/ioCsEUnZ+pQya6zoiyChazGL/JjrZ4fuVlNd3
-czmfPtxKGf7L4Ecv8aGj1ZBiuZpE8BEuJSPAj1fn8tKonDDBqRxBWUkng/e6cV6aTKKXHtlNUWWJ
-3wdtoDyZS20c2ZoV+SLaFiYn4y44mGM2qY5TXoOSLtBvxgG8WhUTXfIgJ1CG14qw8XXNwHFWrCxB
-RUXl/HHaGeK47Ubx5ngCPHmt9eDEJ8aIiTex/hh1cseAyR8Mg367VWwYdiuG+4RaSebzs7+jFb7/
-Qqd+g6mF1Uz2LnK3rfX08dulhcFl3vwL0SyW+At+C2qe
+eJx9VNtO4zAQffdXDKEiUEFhX8t22bJFWqRyEVuQVkKy3Hi6sZQ44Dit+sK379hJittG5KGqPZdz
+fOZyCLNUlbBQGUJelRbmCFWJElbKphCVRWUShLnS5yKxaiksDpIyjaC/MEUO9Lc/YIfwt6ggEVoX
+FkylQVmQymBis7Wz/jJIcRLma5iIpZIIEwXXmSgVfJf+Qs5//suFygZJkf8YMFaiBY2rTGkcxa8s
+ZkxkSpQgsWUBsUVi27viD9MJf7l9mj2Pp/xxPPsNByO4gKMjoCSol+Dvot6e3/A9cl6VdmB71ksw
+mIoyvYROnKeHu8dZiARvpMebHe0CeccvoLz9sjY5tq3h5v6lgY5eD4b9yGFFutCSrkzlRMAm554y
+we3bWhYJqXcIzx5bGYMZLoW2sBRGiXmG5YAFsdsIvhA7rCDiPDhyHtXl2lOQpGhkZtuVCKKH7+ec
+X9/e8/vx3Q3nw00EfWoBxwFWrRTBeSWiE7Apagb0OXRKz7XIEUbQFcMwK7HLOT6OtwlZQo9PIGao
+pVrULKj64Ysnt3/G19ObtgkCJrXzF74jRz2MaCnJgtcN5B7wLfK2DedOp4vGydPcet5urq2XBEZv
+DcnQpBZVJt0KUBqEa4YzpS0a3x7odFOm0Dlqe9oEkN8qVUlK01/iKfSa3LRRKmqkBc2vBKFpmyCs
+XG4d2yYyEQZBzIvKOgLN+JDveiVoaXyqedVYOkTrmCRqutrfNVHr6xMFBhh9QD/qNQuGLvq72d03
+3Jy2CtGCf0rca/tp+N4BXqsflKquRr0L2sjmuClOu+/8/NKvTQsNZ3l9ZqxeTew//1a6EA==
 """
 )
 
 # file activate.xsh
 ACTIVATE_XSH = convert(
     """
-eJyFU11rwjAUfc+vuIt9sMz1Bwg+OCYoaB2bkw2RkNlbLdSkJLFsjP33JdV+aN2Wh5L7eW7PuaGU
-vkqhd8A3Jsm5QdAblWQGYqkgT5Q58BRFTiklsZJ7+HDJgZEy1ZDsM6kMbNEwjRlwDex0JyTCGFiE
-ZdcuV1vt9wnYk8RAs89IbigkAniacI36GHInwrR0rk55a1IWel9BEHwHFqZL2Xz6wJaTp8XLcMoe
-h4sx7QGlft3Jc04YgNfKPAO7Ev4f7m0xnofj+WzUBq1Cbegq9NcAdVJFVxkbhcuCtONc55x5jaS6
-UkgRoTbq4IRACkKagnUrR13egWdMYygTb65rUavpBCEdOAiNtptSmGLOhYGcq4S/p6hJU/rV5RBr
-n1xtavlq1BHS/CMbU5SxhocxalNa2jnSCw29prXqr4+OgEdR96zxbbW1Xd8aFuR+ErJwOBtZhB7Y
-rRdmsFAH7IHCLOUbLCyfkIsFub4TJU2NtbB11lNEf5O+mPt8WwqNm8tx+UhsjbubnRRugLu9+5YP
-6AcvDiI9
+eJyNU11PwjAUfe+vuNY9sIj7ASQ+YCSBRD6i02gIaSq7gyWjXdqyaIz/3XYwVmB+9GFZ78c57T2n
+lNIXKfQa+NJkJTcIeqmywkAqFZSZMlueoygppSRVcgPvrjgyUuYask0hlYEVGqaxAK6B7f8JSTAF
+lmCN2uFqpcMeAbuyFGjxkcglhUwAzzOuUe9SbiWY18H5vm5B6sbgM4qir8jSdCib3t+x59FD/NS/
+Z7N+PKRdoDRskAIXhBsIziqPyFrSf9O9xsPpZDgdD85JD6lz6kPqtwM0RYdx1bnB5Lka2u5cxzML
+vKLWTjZ7mI5n8b8A9rUNjpAiQW3U1gmKFIQ0lXpW1gblEh4xT6EuvGjXtHGFE5ZcwlZotGhKYY4l
+FwZKrjL+lqMmvoXmp4dYhKQV1M7d6yPEv5jNKcqYf1VGbcmZB5x4lRcCfzfvLXaBiCdJ5wj46uD+
+Tmg3luR2NGGT/nhgGbpgX48wN7HaYhcUFjlfYrULCTkxWru36jF59rJ9NlJlf7JQde5j11VS+yZr
+0d22eUPaxdycLKMTvqWjR3610emDtgTu36ylcJe83rhv/di/AYN1UZY=
 """
 )
 
 # file activate.bat
 ACTIVATE_BAT = convert(
     """
-eJx9Ul9LhEAQfxf8DoOclI/dYyFkaCmcq4gZQTBUrincuZFbff12T133TM+nnd35/Zvxlr7XDFhV
-mUZHOVhFlOWP3g4DUriIWoVomYZpNBWUtGpaWgImO191pFkSpzlcmgaI70jVX7n2Qp8tuByg+46O
-CMHbMq64T+nmlJt082D1T44muCDk2prgEHF4mdI9RaS/QwSt3zSyIAaftRccvqVTBziD1x/WlPD5
-xd729NDBb8Nr4DU9QNMKsJeH9pkhPedhQsIkDuCDCa6A+NF9IevVFAohkqizdHetg/tkWvPoftWJ
-MCqnOxv7/x7Np6yv9P2Ker5dmX8yNyCkkWnbZy3N5LarczlqL8htx2EM9rQ/2H5BvIsIEi8OEG8U
-+g8CsNTr
+eJyVk1FLhEAUhd8X/A8XWSkf28dCyMUpBR3FzAiCS+WYwq4TOdXfb0Z3dTJdyCfveO85n8frNXut
+OPCyNFbGqmUCzDxIs3s3REJzB1GrEE3VVJdQsLJuWAEYh97QkaRxlGRwbqxAXp1Uf+RYM32W1LKB
+7Vp2nJC6DReD9m+5qeQ6Wd+a/SN7dlzn9oI7dxsSXJCcoXOskfLgYXdv/j8LnXiM8iGg/RmiZmOr
+bFMSgcebMwGfKhgbBIfnL14X8P7BX3Zs38J3LSoQFdtD3YCVuJlvnfgmj5kfUz+OCLxxqUWoF9zk
+qtYAFyZkBsO9ArzUh/td0ZqP9IskElTFMsnwb4/GqeoLPUlZT5dJvf8Id5hQIONynUSa2G0Wc+m8
+Z+w2w4/Tt2hbYT0hbgOK1I0I4tUw/QOTZfLE
 """
 )
 
@@ -2329,17 +2330,18 @@ def convert(s):
 # file activate.ps1
 ACTIVATE_PS = convert(
     """
-eJyNVMtu2zAQvOsrNrLQ2miloFcXPdiwgRhwHCN2c2kLgqZWMQGKFEhKqVHky3LoJ/UXSlqWX3LS
-8ibtzHJ2d5Z/Xn53YLnmBjIuEPLSWFghpMqCUaVmmEKmVQ5ztVh/ho0qgVEpXVSXEriFlGtkVmwS
-GCmwLk8fEkiuKbO8ohaTwnwKgsgwzQvbX95MFmQ+WN7AF4jyDZeVYtRyJZN8w1SeU5kmBbXrPWE4
-WIzJaHLv8KYQ3MY+Cl2NRokK668w2qe9TpKwB/GcapQ2CLJSMp8dHoVaUdFPsZHV/WaeuGXrHxDN
-lByhsbr0IewFvwJwh2fQte53fUVFNacrgX1yNx2Rh8n98utgur2xt0XXHH8ilFW/qfB12h6vMVeu
-kAYJYQsaQmyYKnBXxJb5HFwQ2VTbJ0qkpOLallSQwg2vsC2Ze3Ad92rf4p/r5Rbzw4XfX2Mc6dw2
-pqlrPHtoKfIpHOZ00ucsiAXS7KKaFhK1VprWBjDO29K5lClpuSzxXN1Vywan6jqwQJFBukNcvd2P
-g8/exhWbVLGdlOe2XetwLaLY2LWLxDls/0JE9aPxpA6U0qAFrjUKrKi0e7ea4CAEYqlkeijQ7eRx
-s9z4m1ULWj13waNPx9zpa1nVIxv/B8ebEJ7nvCZkOJmR2eB2TNzxMLIYzwkJ4cNRjno0Z1wncjEY
-Tsdkfn93O182G3vevdc8eRhqGO56f7oRF4gn63GUqzWxS9d0YJCmQKHQmPGfYP0zicBK7R8pqCkf
-YVW6t1TJ9/5FNYzq1D2uyT7Hk3bOidfKPc5hN+r+e0Wg14NwO3R8ElwejPjuPxbdu/EvkRDrCw==
+eJytVcFu2zAMvfsrWNfYEmx2sWuGHVIkQAO0adBkvWyDoMh0I0CWDElOGwz598p2HDt20w3YdLP5
+SD2Sj9QlrDbcQMIFQpobC2uEWFkwKtcMY0i0SmGhlpuvsFM5MCqls+pcArcQc43Mil0EEwXWxRlB
+BNEVZZZvqcUoM188LzBM88yOVjezJVmMVzfwDYJ0x+VWMWq5klG6YypNqYyjjNrN0eF6vJySyezB
+4U0muA0LKww0GiW2WH35wTHsVRT5QwgXVKO0npfkkhXR4UmoNRWjGGtagx/mmVu2+QXBXMkJGqvz
+woRD77cH7vAEBtb9rq7YUs3pWuCI3N9OyOPsYfV9fFveOCzRlU9xApTbUZ3hebcjXmOqXCI1Evwe
+1IfQMJXhIYnSc++9QbLOdkSUiMmWa5tTQTLXvMz2aB7Blb1g+55/ly+3mDYX/jzn0eJZFqbOazp/
+7DEqQjjMaae7XhAKpMmbbHpI1FppWgnAOG1Lp1KmpOUyxy67i54MTtldwhJFAvEBcfF+PRqdvY/L
+drFiByr7vlwrc0Ui29mNs4QplH8hoPrJFE6XkEuDFrjWKHBLpT2q1XgNEQilknGToJvJdrFc++tR
+83o1d8bWp/M88OtJtUDW+vfak+B3Y14Rcj2bk/n4bkrcKWBkOV0Q4sOnVoyqNR1fR3I5vr6dksXD
+/d1iVU9st3rnNNk01fcPtT+diDccT8ajFavg55OmBiWhIolQulH2uyrqNfh0thpgJblxHLuNisDc
+TnYbONOY8BewqvyJL9w4KT9BJ0hxnrXTWbhRbpWfYackPgsu8cTtw1/ugH2rbCgM/nuWtJMcy3Wx
+wQ+5fYZ17h4aJT8Wz41hVMfu5YnOpjwIBn/eITAcwn+rxN57BRHvOEk=
 """
 )
 


Test patch:
diff --git a/tests/activation/test_prompts.py b/tests/activation/test_prompts.py
new file mode 100644
--- /dev/null
+++ b/tests/activation/test_prompts.py
@@ -0,0 +1,440 @@
+"""test that prompt behavior is correct in supported shells"""
+from __future__ import absolute_import, unicode_literals
+
+import os
+import subprocess
+import sys
+from textwrap import dedent
+
+import pytest
+
+import virtualenv
+from virtualenv import IS_DARWIN, IS_WIN
+
+try:
+    from pathlib import Path
+except ImportError:
+    from pathlib2 import Path
+
+VIRTUAL_ENV_DISABLE_PROMPT = "VIRTUAL_ENV_DISABLE_PROMPT"
+
+# This must match the DEST_DIR provided in the ../conftest.py:clean_python fixture
+ENV_DEFAULT = "env"
+
+# This can be anything
+ENV_CUSTOM = "env_custom"
+
+# Standard prefix, surround the env name in parentheses and separate by a space
+PREFIX_DEFAULT = "({}) ".format(ENV_DEFAULT)
+
+# Arbitrary prefix for the environment that's provided a 'prompt' arg
+PREFIX_CUSTOM = "---ENV---"
+
+# Temp script filename template: {shell}.script.(normal|suppress).(default|custom)[extension]
+SCRIPT_TEMPLATE = "{}.script.{}.{}{}"
+
+# Temp output filename template: {shell}.out.(normal|suppress).(default|custom)
+OUTPUT_TEMPLATE = "{}.out.{}.{}"
+
+# For skipping shells not installed by default if absent on a contributor's system
+IS_INSIDE_CI = "CI_RUN" in os.environ
+
+
+# Py2 doesn't like unicode in the environment
+def env_compat(string):
+    return string.encode("utf-8") if sys.version_info.major < 3 else string
+
+
+class ShellInfo(object):
+    """Parent class for shell information for prompt testing."""
+
+    # Typo insurance
+    __slots__ = []
+
+    # Equality check based on .name, but only if both are not None
+    def __eq__(self, other):
+        if type(self) != type(other):
+            return False
+        if self.name is None or other.name is None:
+            return False
+        return self.name == other.name
+
+    # Helper formatting string
+    @property
+    def platform_incompat_msg(self):
+        return "No sane provision for {} on {{}} yet".format(self.name)
+
+    # Each shell must specify
+    name = None
+    avail_cmd = None
+    execute_cmd = None
+    prompt_cmd = None
+    activate_script = None
+
+    # Default values defined here
+    # 'preamble_cmd' *MUST NOT* emit anything to stdout!
+    testscript_extension = ""
+    preamble_cmd = ""
+    activate_cmd = "source "
+    deactivate_cmd = "deactivate"
+    clean_env_update = {}
+
+    # Skip check function; must be specified per-shell
+    platform_check_skip = None
+
+    # Test assert method for comparing activated prompt to deactivated.
+    # Default defined, but can be overridden per-shell. Takes the captured
+    # lines of output as the lone argument.
+    def overall_prompt_test(self, lines, prefix):
+        """Perform all tests on (de)activated prompts.
+
+        From a Python 3 perspective, 'lines' is expected to be *bytes*,
+        and 'prefix' is expected to be *str*.
+
+        Happily, this all seems to translate smoothly enough to 2.7.
+
+        """
+        # Prompts before activation and after deactivation should be identical.
+        assert lines[1] == lines[3], lines
+
+        # The .partition here operates on the environment marker text expected to occur
+        # in the prompt. A non-empty 'env_marker' thus tests that the correct marker text
+        # has been applied into the prompt string.
+        before, env_marker, after = lines[2].partition(prefix.encode("utf-8"))
+        assert env_marker != b"", lines
+
+        # Some shells need custom activated-prompt tests, so this is split into
+        # its own submethod.
+        self.activated_prompt_test(lines, after)
+
+    def activated_prompt_test(self, lines, after):
+        """Perform just the check for the deactivated prompt contents in the activated prompt text.
+
+        The default is a strict requirement that the portion of the activated prompt following the environment
+        marker must exactly match the non-activated prompt.
+
+        Some shells require weaker tests, due to idiosyncrasies.
+
+        """
+        assert after == lines[1], lines
+
+
+class BashInfo(ShellInfo):
+    name = "bash"
+    avail_cmd = "bash -c 'echo foo'"
+    execute_cmd = "bash"
+    prompt_cmd = 'echo "$PS1"'
+    activate_script = "activate"
+
+    def platform_check_skip(self):
+        if IS_WIN:
+            return self.platform_incompat_msg.format(sys.platform)
+
+
+class FishInfo(ShellInfo):
+    name = "fish"
+    avail_cmd = "fish -c 'echo foo'"
+    execute_cmd = "fish"
+    prompt_cmd = "fish_prompt; echo ' '"
+    activate_script = "activate.fish"
+
+    # Azure Devops doesn't set a terminal type, which breaks fish's colorization
+    # machinery in a way that spuriously fouls the activation script.
+    clean_env_update = {"TERM": "linux"}
+
+    def platform_check_skip(self):
+        if IS_WIN:
+            return self.platform_incompat_msg.format(sys.platform)
+
+    def activated_prompt_test(self, lines, after):
+        """Require a looser match here, due to interposed ANSI color codes.
+
+        This construction allows coping with the messiness of fish's ANSI codes for colorizing.
+        It's not as rigorous as I would like---it doesn't ensure no space is inserted between
+        a custom env prompt (argument to --prompt) and the base prompt---but it does provide assurance as
+        to the key pieces of content that should be present.
+
+        """
+        assert lines[1] in after, lines
+
+
+class CshInfo(ShellInfo):
+    name = "csh"
+    avail_cmd = "csh -c 'echo foo'"
+    execute_cmd = "csh"
+    prompt_cmd = r"set | grep -E 'prompt\s' | sed -E 's/^prompt\s+(.*)$/\1/'"
+    activate_script = "activate.csh"
+
+    # csh defaults to an unset 'prompt' in non-interactive shells
+    preamble_cmd = "set prompt=%"
+
+    def platform_check_skip(self):
+        if IS_WIN:
+            return self.platform_incompat_msg.format(sys.platform)
+
+    def activated_prompt_test(self, lines, after):
+        """Test with special handling on MacOS, which does funny things to stdout under (t)csh."""
+        if IS_DARWIN:
+            # Looser assert for (t)csh on MacOS, which prepends extra text to
+            # what gets sent to stdout
+            assert lines[1].endswith(after), lines
+        else:
+            # Otherwise, use the rigorous default
+            # Full 2-arg form for super() used for 2.7 compat
+            super(CshInfo, self).activated_prompt_test(lines, after)
+
+
+class XonshInfo(ShellInfo):
+    name = "xonsh"
+    avail_cmd = "xonsh -c 'echo foo'"
+    execute_cmd = "xonsh"
+    prompt_cmd = "print(__xonsh__.shell.prompt)"
+    activate_script = "activate.xsh"
+
+    # Sets consistent initial state
+    preamble_cmd = (
+        "$VIRTUAL_ENV = ''; $PROMPT = '{env_name}$ '; "
+        "$PROMPT_FIELDS['env_prefix'] = '('; $PROMPT_FIELDS['env_postfix'] = ') '"
+    )
+
+    def platform_check_skip(self):
+        if IS_WIN:
+            return "Provisioning xonsh on windows is unreliable"
+
+        if sys.version_info < (3, 4):
+            return "xonsh requires Python 3.4 at least"
+
+
+class CmdInfo(ShellInfo):
+    name = "cmd"
+    avail_cmd = "echo foo"
+    execute_cmd = ""
+    prompt_cmd = "echo %PROMPT%"
+    activate_script = "activate.bat"
+
+    testscript_extension = ".bat"
+    preamble_cmd = "@echo off & set PROMPT=$P$G"  # For consistent initial state
+    activate_cmd = "call "
+    deactivate_cmd = "call deactivate"
+
+    def platform_check_skip(self):
+        if not IS_WIN:
+            return self.platform_incompat_msg.format(sys.platform)
+
+
+class PoshInfo(ShellInfo):
+    name = "posh"
+    avail_cmd = "powershell 'echo foo'"
+    execute_cmd = "powershell -File "
+    prompt_cmd = "prompt"
+    activate_script = "activate.ps1"
+
+    testscript_extension = ".ps1"
+    activate_cmd = ". "
+
+    def platform_check_skip(self):
+        if not IS_WIN:
+            return self.platform_incompat_msg.format(sys.platform)
+
+
+SHELLINFO_LIST = [BashInfo(), FishInfo(), CshInfo(), XonshInfo(), CmdInfo(), PoshInfo()]
+
+
+@pytest.fixture(scope="module")
+def posh_execute_enabled(tmp_path_factory):
+    """Return check value for whether Powershell script execution is enabled.
+
+    Posh may be available interactively, but the security settings may not allow
+    execution of script files.
+
+    # Enable with:  PS> Set-ExecutionPolicy -scope currentuser -ExecutionPolicy Bypass -Force;
+    # Disable with: PS> Set-ExecutionPolicy -scope currentuser -ExecutionPolicy Restricted -Force;
+
+    """
+    if not IS_WIN:
+        return False
+
+    test_ps1 = tmp_path_factory.mktemp("posh_test") / "test.ps1"
+    with open(str(test_ps1), "w") as f:
+        f.write("echo 'foo bar baz'\n")
+
+    out = subprocess.check_output(["powershell", "-File", "{}".format(str(test_ps1))], shell=True)
+    return b"foo bar baz" in out
+
+
+@pytest.fixture(scope="module")
+def shell_avail(posh_execute_enabled):
+    """Generate mapping of ShellInfo.name strings to bools of shell availability."""
+    retvals = {si.name: subprocess.call(si.avail_cmd, shell=True) for si in SHELLINFO_LIST}
+    avails = {si.name: retvals[si.name] == 0 for si in SHELLINFO_LIST}
+
+    # Extra check for whether powershell scripts are enabled
+    avails[PoshInfo().name] = avails[PoshInfo().name] and posh_execute_enabled
+
+    return avails
+
+
+@pytest.fixture(scope="module")
+def custom_prompt_root(tmp_path_factory):
+    """Provide Path to root with default and custom venvs created."""
+    root = tmp_path_factory.mktemp("custom_prompt")
+    virtualenv.create_environment(
+        str(root / ENV_CUSTOM), prompt=PREFIX_CUSTOM, no_setuptools=True, no_pip=True, no_wheel=True
+    )
+
+    _, _, _, bin_dir = virtualenv.path_locations(str(root / ENV_DEFAULT))
+
+    bin_dir_name = os.path.split(bin_dir)[-1]
+
+    return root, bin_dir_name
+
+
+@pytest.fixture(scope="module")
+def clean_python_root(clean_python):
+    root = Path(clean_python[0]).resolve().parent
+    bin_dir_name = os.path.split(clean_python[1])[-1]
+
+    return root, bin_dir_name
+
+
+@pytest.fixture(scope="module")
+def get_work_root(clean_python_root, custom_prompt_root):
+    def pick_root(env):
+        if env == ENV_DEFAULT:
+            return clean_python_root
+        elif env == ENV_CUSTOM:
+            return custom_prompt_root
+        else:
+            raise ValueError("Invalid test virtualenv")
+
+    return pick_root
+
+
+@pytest.fixture(scope="function")
+def clean_env():
+    """Provide a fresh copy of the shell environment.
+
+    VIRTUAL_ENV_DISABLE_PROMPT is always removed, if present, because
+    the prompt tests assume it to be unset.
+
+    """
+    clean_env = os.environ.copy()
+    clean_env.pop(env_compat(VIRTUAL_ENV_DISABLE_PROMPT), None)
+    return clean_env
+
+
+SHELLINFO_LIST = [BashInfo(), FishInfo(), CshInfo(), XonshInfo(), CmdInfo(), PoshInfo()]
+
+
+@pytest.mark.parametrize("shell_info", SHELLINFO_LIST, ids=(lambda si: si.name))
+@pytest.mark.parametrize("env", [ENV_DEFAULT, ENV_CUSTOM])
+@pytest.mark.parametrize(("value", "disable"), [("", False), ("0", True), ("1", True)])
+def test_suppressed_prompt(shell_info, shell_avail, env, value, disable, get_work_root, clean_env):
+    """Confirm non-empty VIRTUAL_ENV_DISABLE_PROMPT suppresses prompt changes on activate."""
+    skip_test = shell_info.platform_check_skip()
+    if skip_test:
+        pytest.skip(skip_test)
+
+    if not IS_INSIDE_CI and not shell_avail[shell_info.name]:
+        pytest.skip(
+            "Shell '{}' not provisioned".format(shell_info.name)
+            + (" - is Powershell script execution disabled?" if shell_info == PoshInfo() else "")
+        )
+
+    script_name = SCRIPT_TEMPLATE.format(shell_info.name, "suppress", env, shell_info.testscript_extension)
+    output_name = OUTPUT_TEMPLATE.format(shell_info.name, "suppress", env)
+
+    clean_env.update({env_compat(VIRTUAL_ENV_DISABLE_PROMPT): env_compat(value)})
+
+    work_root = get_work_root(env)
+
+    # The extra "{prompt}" here copes with some oddity of xonsh in certain emulated terminal
+    # contexts: xonsh can dump stuff into the first line of the recorded script output,
+    # so we have to include a dummy line of output that can get munged w/o consequence.
+    with open(str(work_root[0] / script_name), "w") as f:
+        f.write(
+            dedent(
+                """\
+        {preamble}
+        {prompt}
+        {prompt}
+        {act_cmd}{env}/{bindir}/{act_script}
+        {prompt}
+    """.format(
+                    env=env,
+                    act_cmd=shell_info.activate_cmd,
+                    preamble=shell_info.preamble_cmd,
+                    prompt=shell_info.prompt_cmd,
+                    act_script=shell_info.activate_script,
+                    bindir=work_root[1],
+                )
+            )
+        )
+
+    command = "{} {} > {}".format(shell_info.execute_cmd, script_name, output_name)
+
+    assert 0 == subprocess.call(command, cwd=str(work_root[0]), shell=True, env=clean_env)
+
+    with open(str(work_root[0] / output_name), "rb") as f:
+        lines = f.read().split(b"\n")
+
+    # Is the prompt suppressed based on the env var value?
+    assert (lines[1] == lines[2]) == disable, lines
+
+
+@pytest.mark.parametrize("shell_info", SHELLINFO_LIST, ids=(lambda si: si.name))
+@pytest.mark.parametrize(["env", "prefix"], [(ENV_DEFAULT, PREFIX_DEFAULT), (ENV_CUSTOM, PREFIX_CUSTOM)])
+def test_activated_prompt(shell_info, shell_avail, env, prefix, get_work_root, clean_env):
+    """Confirm prompt modification behavior with and without --prompt specified."""
+    skip_test = shell_info.platform_check_skip()
+    if skip_test:
+        pytest.skip(skip_test)
+
+    if not IS_INSIDE_CI and not shell_avail[shell_info.name]:
+        pytest.skip(
+            "Shell '{}' not provisioned".format(shell_info.name)
+            + (" - is Powershell script execution disabled?" if shell_info == PoshInfo() else "")
+        )
+
+    for k, v in shell_info.clean_env_update.items():
+        clean_env.update({env_compat(k): env_compat(v)})
+
+    script_name = SCRIPT_TEMPLATE.format(shell_info.name, "normal", env, shell_info.testscript_extension)
+    output_name = OUTPUT_TEMPLATE.format(shell_info.name, "normal", env)
+
+    work_root = get_work_root(env)
+
+    # The extra "{prompt}" here copes with some oddity of xonsh in certain emulated terminal
+    # contexts: xonsh can dump stuff into the first line of the recorded script output,
+    # so we have to include a dummy line of output that can get munged w/o consequence.
+    with open(str(work_root[0] / script_name), "w") as f:
+        f.write(
+            dedent(
+                """\
+        {preamble}
+        {prompt}
+        {prompt}
+        {act_cmd}{env}/{bindir}/{act_script}
+        {prompt}
+        {deactivate}
+        {prompt}
+        """.format(
+                    env=env,
+                    act_cmd=shell_info.activate_cmd,
+                    deactivate=shell_info.deactivate_cmd,
+                    preamble=shell_info.preamble_cmd,
+                    prompt=shell_info.prompt_cmd,
+                    act_script=shell_info.activate_script,
+                    bindir=work_root[1],
+                )
+            )
+        )
+
+    command = "{} {} > {}".format(shell_info.execute_cmd, script_name, output_name)
+
+    assert 0 == subprocess.call(command, cwd=str(work_root[0]), shell=True, env=clean_env)
+
+    with open(str(work_root[0] / output_name), "rb") as f:
+        lines = f.read().split(b"\n")
+
+    shell_info.overall_prompt_test(lines, prefix)