Repository: tox-dev/tox

Issue:
additional environments config order is not kept
Our own config shows this:

```
using tox.ini: /home/bernat/git/tox/tox.ini
using tox-3.1.2 from /home/bernat/.local/lib/python2.7/site-packages/tox/__init__.pyc
default environments:
py27                -> run the tests with pytest under python2.7
py34                -> run the tests with pytest under python3.4
py35                -> run the tests with pytest under python3.5
py36                -> run the tests with pytest under python3.6
py37                -> run the tests with pytest under python3.7
pypy                -> run the tests with pytest under pypy
coverage            -> [run locally after tests]: combine coverage data and create report; generates a diff coverage against origin/master (can be changed by setting DIFF_AGAINST env var)
fix-lint            -> format the code base to adhere to our styles, and complain about what we cannot do automatically
docs                -> invoke sphinx-build to build the HTML docs and check that all links are valid
package-description -> check that the long description is valid

additional environments:
X                   -> print the positional arguments passed in with echo
codecov             -> [only run on CI]: upload coverage data to codecov (depends on coverage running first)
dev                 -> generate a DEV environment
exit_code           -> commands with several exit codes
notify              -> notify people about the release of the library
release             -> do a release, required posarg of the version number
```

Even though dev is last it's not displayed last.

Discussion:

Patch:
diff --git a/changelog/921.feature.rst b/changelog/921.feature.rst
new file mode 100644
index 00000000..7a061b95
--- /dev/null
+++ b/changelog/921.feature.rst
@@ -0,0 +1,1 @@
+keep additional environments config order when listing them - by :user:`gaborbernat`
diff --git a/doc/config.rst b/doc/config.rst
index dc89ca09..e56fe8de 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -189,7 +189,7 @@ Complete list of settings that you can put into ``testenv*`` sections:
 
     **default**::
 
-        pip install {opts} {packages}
+        python -m pip install {opts} {packages}
 
 .. confval:: list_dependencies_command
 
diff --git a/src/tox/config.py b/src/tox/config.py
index e9f55c99..a7e62d52 100755
--- a/src/tox/config.py
+++ b/src/tox/config.py
@@ -10,6 +10,7 @@ import string
 import sys
 import uuid
 import warnings
+from collections import OrderedDict
 from fnmatch import fnmatchcase
 from subprocess import list2cmdline
 
@@ -783,7 +784,7 @@ class Config(object):
     """Global Tox config object."""
 
     def __init__(self, pluginmanager, option, interpreters, parser):
-        self.envconfigs = {}
+        self.envconfigs = OrderedDict()
         """Mapping envname -> envconfig"""
         self.invocationcwd = py.path.local()
         self.interpreters = interpreters
@@ -1110,25 +1111,28 @@ class ParseIni(object):
         env_list = _split_env(env_str)
 
         # collect section envs
-        all_envs = set(env_list) - {"ALL"}
+        all_envs = OrderedDict((i, None) for i in env_list)
+        if "ALL" in all_envs:
+            all_envs.pop("ALL")
         for section in self._cfg:
             if section.name.startswith(testenvprefix):
-                all_envs.add(section.name[len(testenvprefix) :])
+                all_envs[section.name[len(testenvprefix) :]] = None
         if not all_envs:
-            all_envs.add("python")
+            all_envs["python"] = None
 
         package_env = config.isolated_build_env
         if config.isolated_build is True and package_env in all_envs:
-            all_envs.remove(package_env)
+            all_envs.pop(package_env)
 
         if not env_list or "ALL" in env_list:
-            env_list = sorted(all_envs)
+            env_list = list(all_envs.keys())
 
         if config.isolated_build is True and package_env in env_list:
             msg = "isolated_build_env {} cannot be part of envlist".format(package_env)
             raise tox.exception.ConfigError(msg)
 
-        return env_list, all_envs
+        all_env_list = list(all_envs.keys())
+        return env_list, all_env_list
 
 
 def _split_env(env):
diff --git a/src/tox/session.py b/src/tox/session.py
index 5ac0c7d3..97e5da19 100644
--- a/src/tox/session.py
+++ b/src/tox/session.py
@@ -621,7 +621,7 @@ class Session:
         env_conf = self.config.envconfigs  # this contains all environments
         default = self.config.envlist  # this only the defaults
         ignore = {self.config.isolated_build_env}.union(default)
-        extra = sorted(e for e in env_conf if e not in ignore) if all_envs else []
+        extra = [e for e in env_conf if e not in ignore] if all_envs else []
 
         if description:
             self.report.line("default environments:")


Test patch:
diff --git a/tests/unit/session/test_list_env.py b/tests/unit/session/test_list_env.py
index 8ccee897..e48e532a 100644
--- a/tests/unit/session/test_list_env.py
+++ b/tests/unit/session/test_list_env.py
@@ -150,3 +150,45 @@ def test_listenvs_packaging_excluded(cmd, initproj):
     result = cmd("-a")
     expected = ["py36", "py27", "py34", "pypi", "docs", "notincluded"]
     assert result.outlines == expected, result.outlines
+
+
+def test_listenvs_all_extra_definition_order_decreasing(cmd, initproj):
+    initproj(
+        "listenvs_all",
+        filedefs={
+            "tox.ini": """
+        [tox]
+        envlist=py36
+
+        [testenv:b]
+        changedir = whatever
+
+        [testenv:a]
+        changedir = docs
+        """
+        },
+    )
+    result = cmd("-a")
+    expected = ["py36", "b", "a"]
+    assert result.outlines == expected
+
+
+def test_listenvs_all_extra_definition_order_increasing(cmd, initproj):
+    initproj(
+        "listenvs_all",
+        filedefs={
+            "tox.ini": """
+        [tox]
+        envlist=py36
+
+        [testenv:a]
+        changedir = whatever
+
+        [testenv:b]
+        changedir = docs
+        """
+        },
+    )
+    result = cmd("-a")
+    expected = ["py36", "a", "b"]
+    assert result.outlines == expected
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
index d4d940f7..8b5d2285 100644
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -1833,9 +1833,9 @@ class TestGlobalOptions:
         assert config.envlist == ["py35", "py36"]
         monkeypatch.setenv("TOXENV", "ALL")
         config = newconfig([], inisource)
-        assert config.envlist == ["py27", "py35", "py36"]
+        assert config.envlist == ["py36", "py35", "py27"]
         config = newconfig(["-eALL"], inisource)
-        assert config.envlist == ["py27", "py35", "py36"]
+        assert config.envlist == ["py36", "py35", "py27"]
         config = newconfig(["-espam"], inisource)
         assert config.envlist == ["spam"]