Repository: conan-io/conan

Issue:
tools.environment_append raises if tries to unset variable which was never set
after #4224, I may use the following code, for instance, to ensure variable is not set:
```
with environment_append({'CONAN_BASH_PATH': None}):
    pass
```
however, it raises if `CONAN_BASH_PATH` is not set (prior to the environment_append invocation):
```
Traceback (most recent call last):
  File "C:\bincrafters\conan\conans\test\unittests\client\tools\os_info\osinfo_test.py", line 39, in test_windows
    with environment_append(new_env):
  File "c:\users\sse4\appdata\local\programs\python\python36\lib\contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "C:\bincrafters\conan\conans\client\tools\env.py", line 57, in environment_append
    os.environ.pop(var)
  File "c:\users\sse4\appdata\local\programs\python\python36\lib\_collections_abc.py", line 795, in pop
    value = self[key]
  File "c:\users\sse4\appdata\local\programs\python\python36\lib\os.py", line 669, in __getitem__
    raise KeyError(key) from None
KeyError: 'CONAN_BASH_PATH'
```
I would expect `tools.environment_append` to be no op in such case, otherwise, it requires additional logic to workaround this behavior.

To help us debug your issue please explain:

- [ ] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've specified the Conan version, operating system version and any tool that can be relevant.
- [ ] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.



Discussion:

Patch:
diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py
index 40816f6b9..9bfcd123b 100644
--- a/conans/client/build/cmake.py
+++ b/conans/client/build/cmake.py
@@ -21,7 +21,7 @@ class CMake(object):
 
     def __init__(self, conanfile, generator=None, cmake_system_name=True,
                  parallel=True, build_type=None, toolset=None, make_program=None,
-                 set_cmake_flags=False, msbuild_verbosity=None):
+                 set_cmake_flags=False, msbuild_verbosity=None, cmake_program=None):
         """
         :param conanfile: Conanfile instance
         :param generator: Generator name to use or none to autodetect
@@ -33,7 +33,8 @@ class CMake(object):
                 applies only to certain generators (e.g. Visual Studio)
         :param set_cmake_flags: whether or not to set CMake flags like CMAKE_CXX_FLAGS, CMAKE_C_FLAGS, etc.
                it's vital to set for certain projects (e.g. using CMAKE_SIZEOF_VOID_P or CMAKE_LIBRARY_ARCHITECTURE)
-        "param msbuild_verbosity: verbosity level for MSBuild (in case of Visual Studio generator)
+        :param msbuild_verbosity: verbosity level for MSBuild (in case of Visual Studio generator)
+        :param cmake_program: Path to the custom cmake executable
         """
         if not isinstance(conanfile, ConanFile):
             raise ConanException("First argument of CMake() has to be ConanFile. Use CMake(self)")
@@ -41,6 +42,7 @@ class CMake(object):
         self._conanfile = conanfile
         self._settings = conanfile.settings
         self._build_type = build_type or conanfile.settings.get_safe("build_type")
+        self._cmake_program = os.getenv("CONAN_CMAKE_PROGRAM") or cmake_program or "cmake"
 
         self.generator = generator or get_generator(conanfile.settings)
         self.parallel = parallel
@@ -187,7 +189,7 @@ class CMake(object):
             pkg_env = {"PKG_CONFIG_PATH": self._conanfile.install_folder} if set_env else {}
 
         with tools.environment_append(pkg_env):
-            command = "cd %s && cmake %s" % (args_to_string([self.build_dir]), arg_list)
+            command = "cd %s && %s %s" % (args_to_string([self.build_dir]), self._cmake_program, arg_list)
             if platform.system() == "Windows" and self.generator == "MinGW Makefiles":
                 with tools.remove_from_path("sh"):
                     self._run(command)
@@ -230,7 +232,7 @@ class CMake(object):
             self.build_config,
             args_to_string(args)
         ])
-        command = "cmake --build %s" % arg_list
+        command = "%s --build %s" % (self._cmake_program, arg_list)
         self._run(command)
 
     def install(self, args=None, build_dir=None):
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 1c3dfae4c..8d8c75ec8 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -105,6 +105,7 @@ request_timeout = 60                  # environment CONAN_REQUEST_TIMEOUT (secon
 # non_interactive = False             # environment CONAN_NON_INTERACTIVE
 
 # conan_make_program = make           # environment CONAN_MAKE_PROGRAM (overrides the make program used in AutoToolsBuildEnvironment.make)
+# conan_cmake_program = cmake         # environment CONAN_CMAKE_PROGRAM (overrides the make program used in CMake.cmake_program)
 
 # cmake_generator                     # environment CONAN_CMAKE_GENERATOR
 # http://www.vtk.org/Wiki/CMake_Cross_Compiling
@@ -205,6 +206,7 @@ class ConanClientConfigParser(ConfigParser, object):
 
                "CONAN_BASH_PATH": self._env_c("general.bash_path", "CONAN_BASH_PATH", None),
                "CONAN_MAKE_PROGRAM": self._env_c("general.conan_make_program", "CONAN_MAKE_PROGRAM", None),
+               "CONAN_CMAKE_PROGRAM": self._env_c("general.conan_cmake_program", "CONAN_CMAKE_PROGRAM", None),
                "CONAN_TEMP_TEST_FOLDER": self._env_c("general.temp_test_folder", "CONAN_TEMP_TEST_FOLDER", "False"),
                "CONAN_SKIP_VS_PROJECTS_UPGRADE": self._env_c("general.skip_vs_projects_upgrade", "CONAN_SKIP_VS_PROJECTS_UPGRADE", "False"),
                "CONAN_HOOKS": self._env_c("hooks", "CONAN_HOOKS", None),
diff --git a/conans/client/conf/detect.py b/conans/client/conf/detect.py
index 612d8dbf3..ccc5734e2 100644
--- a/conans/client/conf/detect.py
+++ b/conans/client/conf/detect.py
@@ -5,6 +5,7 @@ from subprocess import PIPE, Popen, STDOUT
 
 from conans.client.output import Color
 from conans.client.tools.win import latest_visual_studio_version_installed
+from conans.client.tools import OSInfo
 from conans.model.version import Version
 
 
@@ -168,16 +169,11 @@ adjusting 'compiler.libcxx=libstdc++11'
 
 
 def detected_os():
-    result = platform.system()
-    if result == "Darwin":
+    if OSInfo().is_macos:
         return "Macos"
-    if result.startswith("CYGWIN"):
+    if OSInfo().is_windows:
         return "Windows"
-    if result.startswith("MINGW32_NT") or result.startswith("MINGW64_NT"):
-        return "Windows"
-    if result.startswith("MSYS_NT"):
-        return "Windows"
-    return result
+    return platform.system()
 
 
 def _detect_os_arch(result, output):
diff --git a/conans/client/graph/build_mode.py b/conans/client/graph/build_mode.py
index 280607990..e4f9f9fe4 100644
--- a/conans/client/graph/build_mode.py
+++ b/conans/client/graph/build_mode.py
@@ -39,7 +39,7 @@ class BuildMode(object):
                 raise ConanException("--build=never not compatible with other options")
         self._unused_patterns = list(self.patterns)
 
-    def forced(self, conan_file, reference):
+    def forced(self, conan_file, ref):
         if self.never:
             return False
         if self.all:
@@ -50,7 +50,7 @@ class BuildMode(object):
                                    "build_policy='always'")
             return True
 
-        ref = reference.name
+        ref = ref.name
         # Patterns to match, if package matches pattern, build is forced
         for pattern in self.patterns:
             if fnmatch.fnmatch(ref, pattern):
diff --git a/conans/client/rest/rest_client_common.py b/conans/client/rest/rest_client_common.py
index a6631e288..8cace86ca 100644
--- a/conans/client/rest/rest_client_common.py
+++ b/conans/client/rest/rest_client_common.py
@@ -156,7 +156,16 @@ class RestCommonMethods(object):
             response.charset = "utf-8"  # To be able to access ret.text (ret.content are bytes)
             raise get_exception_from_error(response.status_code)(response.text)
 
-        result = json.loads(decode_text(response.content))
+        content = decode_text(response.content)
+        content_type = response.headers.get("Content-Type")
+        if content_type != 'application/json':
+            raise ConanException("%s\n\nResponse from remote is not json, but '%s'"
+                                 % (content, content_type))
+
+        try:  # This can fail, if some proxy returns 200 and an html message
+            result = json.loads(content)
+        except Exception:
+            raise ConanException("Remote responded with broken json: %s" % content)
         if not isinstance(result, dict):
             raise ConanException("Unexpected server response %s" % result)
         return result
diff --git a/conans/client/source.py b/conans/client/source.py
index 751b138fc..f9d2ac13a 100644
--- a/conans/client/source.py
+++ b/conans/client/source.py
@@ -196,7 +196,7 @@ def _run_scm(conanfile, src_folder, local_sources_path, output, cache):
     else:
         # In user space, if revision="auto", then copy
         captured = scm_data.capture_origin or scm_data.capture_revision
-    local_sources_path = local_sources_path if captured else None
+    local_sources_path = local_sources_path if captured and conanfile.develop else None
 
     if local_sources_path:
         excluded = SCM(scm_data, local_sources_path, output).excluded_files
diff --git a/conans/client/tools/env.py b/conans/client/tools/env.py
index 079f549fd..2584065dd 100644
--- a/conans/client/tools/env.py
+++ b/conans/client/tools/env.py
@@ -54,7 +54,7 @@ def environment_append(env_vars):
         old_env = dict(os.environ)
         os.environ.update(env_vars)
         for var in unset_vars:
-            os.environ.pop(var)
+            os.environ.pop(var, None)
         try:
             yield
         finally:
diff --git a/conans/client/tools/oss.py b/conans/client/tools/oss.py
index 909025be8..2c7106e70 100644
--- a/conans/client/tools/oss.py
+++ b/conans/client/tools/oss.py
@@ -85,24 +85,21 @@ class OSInfo(object):
     """
 
     def __init__(self):
+        system = platform.system()
         self.os_version = None
         self.os_version_name = None
-        self.is_linux = platform.system() == "Linux"
+        self.is_linux = system == "Linux"
         self.linux_distro = None
-        self.is_windows = platform.system() == "Windows"
-        self.is_macos = platform.system() == "Darwin"
-        self.is_freebsd = platform.system() == "FreeBSD"
-        self.is_solaris = platform.system() == "SunOS"
+        self.is_msys = system.startswith("MING") or system.startswith("MSYS_NT")
+        self.is_cygwin = system.startswith("CYGWIN_NT")
+        self.is_windows = system == "Windows" or self.is_msys or self.is_cygwin
+        self.is_macos = system == "Darwin"
+        self.is_freebsd = system == "FreeBSD"
+        self.is_solaris = system == "SunOS"
         self.is_posix = os.pathsep == ':'
 
         if self.is_linux:
-            import distro
-            self.linux_distro = distro.id()
-            self.os_version = Version(distro.version())
-            version_name = distro.codename()
-            self.os_version_name = version_name if version_name != "n/a" else ""
-            if not self.os_version_name and self.linux_distro == "debian":
-                self.os_version_name = self.get_debian_version_name(self.os_version)
+            self._get_linux_distro_info()
         elif self.is_windows:
             self.os_version = self.get_win_os_version()
             self.os_version_name = self.get_win_version_name(self.os_version)
@@ -116,6 +113,15 @@ class OSInfo(object):
             self.os_version = Version(platform.release())
             self.os_version_name = self.get_solaris_version_name(self.os_version)
 
+    def _get_linux_distro_info(self):
+        import distro
+        self.linux_distro = distro.id()
+        self.os_version = Version(distro.version())
+        version_name = distro.codename()
+        self.os_version_name = version_name if version_name != "n/a" else ""
+        if not self.os_version_name and self.linux_distro == "debian":
+            self.os_version_name = self.get_debian_version_name(self.os_version)
+
     @property
     def with_apt(self):
         return self.is_linux and self.linux_distro in \
@@ -143,7 +149,7 @@ class OSInfo(object):
         if "opensuse" in self.linux_distro or "sles" in self.linux_distro:
             return True
         return False
-    
+
     @staticmethod
     def get_win_os_version():
         """
@@ -264,7 +270,7 @@ class OSInfo(object):
     @staticmethod
     def uname(options=None):
         options = " %s" % options if options else ""
-        if platform.system() != "Windows":
+        if not OSInfo().is_windows:
             raise ConanException("Command only for Windows operating system")
         custom_bash_path = OSInfo.bash_path()
         if not custom_bash_path:
@@ -291,7 +297,7 @@ class OSInfo(object):
         if "cygwin" in output:
             return CYGWIN
         elif "msys" in output or "mingw" in output:
-            output = OSInfo.uname("-or")
+            output = OSInfo.uname("-r")
             if output.startswith("2"):
                 return MSYS2
             elif output.startswith("1"):
@@ -305,7 +311,6 @@ class OSInfo(object):
 
 
 def cross_building(settings, self_os=None, self_arch=None):
-
     ret = get_cross_building_settings(settings, self_os, self_arch)
     build_os, build_arch, host_os, host_arch = ret
 
diff --git a/conans/server/rest/multipart_encoder.py b/conans/server/rest/multipart_encoder.py
index c2d3e0f8f..6a2dbdc76 100644
--- a/conans/server/rest/multipart_encoder.py
+++ b/conans/server/rest/multipart_encoder.py
@@ -77,7 +77,7 @@ def read_in_chunks(file_object, chunk_size=1024):
 #             yield buf1
 #         else:
         yield buf1
-        #yield buf2
+        # yield buf2
 
 
 if __name__ == "__main__":
diff --git a/conans/server/rest/server.py b/conans/server/rest/server.py
index 2819202c4..5e726df32 100644
--- a/conans/server/rest/server.py
+++ b/conans/server/rest/server.py
@@ -1,6 +1,5 @@
 import bottle
 
-from conans.model.version import Version
 from conans.server.rest.api_v1 import ApiV1
 from conans.server.rest.api_v2 import ApiV2
 
diff --git a/conans/server/service/mime.py b/conans/server/service/mime.py
index 0e7a4ee89..4a503fb41 100644
--- a/conans/server/service/mime.py
+++ b/conans/server/service/mime.py
@@ -6,4 +6,4 @@ def get_mime_type(filepath):
     else:
         mimetype = "auto"
 
-    return mimetype
\ No newline at end of file
+    return mimetype


Test patch:
diff --git a/conans/test/functional/command/remote_verify_ssl_test.py b/conans/test/functional/command/remote_verify_ssl_test.py
index 0ce3500be..f6f842408 100644
--- a/conans/test/functional/command/remote_verify_ssl_test.py
+++ b/conans/test/functional/command/remote_verify_ssl_test.py
@@ -7,6 +7,7 @@ from conans.test.utils.tools import TestClient
 resp = Response()
 resp._content = b'{"results": []}'
 resp.status_code = 200
+resp.headers = {"Content-Type": "application/json"}
 
 
 class RequesterMockTrue(object):
@@ -54,4 +55,3 @@ class VerifySSLTest(unittest.TestCase):
         self.client = TestClient(requester_class=RequesterMockFalse)
         self.client.run("remote add myremote https://localhost False")
         self.client.run("search op* -r myremote")
-
diff --git a/conans/test/functional/old/download_retries_test.py b/conans/test/functional/old/download_retries_test.py
index 216ea5189..5be5feb21 100644
--- a/conans/test/functional/old/download_retries_test.py
+++ b/conans/test/functional/old/download_retries_test.py
@@ -38,7 +38,7 @@ class MyConanfile(ConanFile):
             ok = None
             status_code = None
             charset = None
-            headers = {}
+            headers = {"Content-Type": "application/json"}
 
             def __init__(self, ok, status_code):
                 self.ok = ok
diff --git a/conans/test/functional/remote/server_error_test.py b/conans/test/functional/remote/server_error_test.py
new file mode 100644
index 000000000..9ae0b47de
--- /dev/null
+++ b/conans/test/functional/remote/server_error_test.py
@@ -0,0 +1,63 @@
+import unittest
+
+
+from conans.test.utils.tools import TestClient, TestServer
+from collections import namedtuple
+
+
+class Error200NoJson(unittest.TestCase):
+
+    def test_error_no_json(self):
+        class RequesterMock(object):
+            def __init__(self, *args, **kwargs):
+                pass
+
+            def get(self, *args, **kwargs):  # @UnusedVariable
+                # Response must be binary, it is decoded in RestClientCommon
+                return namedtuple("Response", "status_code headers content")(200, {}, b'<>')
+
+        # https://github.com/conan-io/conan/issues/3432
+        client = TestClient(servers={"default": TestServer()},
+                            requester_class=RequesterMock,
+                            users={"default": [("lasote", "mypass")]})
+
+        client.run("install pkg/ref@user/testing", assert_error=True)
+        self.assertIn("ERROR: <>", client.out)
+        self.assertIn("Response from remote is not json, but 'None'", client.out)
+
+    def test_error_broken_json(self):
+        class RequesterMock(object):
+            def __init__(self, *args, **kwargs):
+                pass
+
+            def get(self, *args, **kwargs):  # @UnusedVariable
+                # Response must be binary, it is decoded in RestClientCommon
+                headers = {"Content-Type": "application/json"}
+                return namedtuple("Response", "status_code headers content")(200, headers, b'<>')
+
+        # https://github.com/conan-io/conan/issues/3432
+        client = TestClient(servers={"default": TestServer()},
+                            requester_class=RequesterMock,
+                            users={"default": [("lasote", "mypass")]})
+
+        client.run("install pkg/ref@user/testing", assert_error=True)
+        self.assertIn("Remote responded with broken json: <>", client.out)
+
+    def test_error_json(self):
+        class RequesterMock(object):
+            def __init__(self, *args, **kwargs):
+                pass
+
+            def get(self, *args, **kwargs):  # @UnusedVariable
+                # Response must be binary, it is decoded in RestClientCommon
+                headers = {"Content-Type": "application/json"}
+                return namedtuple("Response", "status_code headers content")(200, headers,
+                                                                             b'[1, 2, 3]')
+
+        # https://github.com/conan-io/conan/issues/3432
+        client = TestClient(servers={"default": TestServer()},
+                            requester_class=RequesterMock,
+                            users={"default": [("lasote", "mypass")]})
+
+        client.run("install pkg/ref@user/testing", assert_error=True)
+        self.assertIn("ERROR: Unexpected server response [1, 2, 3]", client.out)
diff --git a/conans/test/functional/scm/test_local_modified.py b/conans/test/functional/scm/test_local_modified.py
new file mode 100644
index 000000000..99c4a0e4c
--- /dev/null
+++ b/conans/test/functional/scm/test_local_modified.py
@@ -0,0 +1,57 @@
+# coding=utf-8
+
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.test.utils.tools import TestClient, create_local_git_repo
+
+
+class SCMFolderObsoleteTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""\
+        from conans import ConanFile, tools
+
+        class Pkg(ConanFile):
+            scm = {"type": "git",
+                   "url": "auto",
+                   "revision": "auto"}
+
+            def build(self):
+                content = tools.load("file.txt")
+                self.output.info(">>>> I'm {}/{}@{}/{}".format(self.name, self.version,
+                                                               self.user, self.channel))
+                self.output.info(">>>> content: {} ".format(content))
+        """)
+
+    def setUp(self):
+        self.reference = "pkg/v1@user/channel"
+        self.t = TestClient(path_with_spaces=False)
+
+        # Create pkg/v1
+        url, _ = create_local_git_repo(files={'conanfile.py': self.conanfile,
+                                              'file.txt': self.reference},
+                                       folder=self.t.current_folder)
+        self.t.runner('git remote add origin {}'.format(url), cwd=self.t.current_folder)
+        self.t.run("create . {}".format(self.reference))
+        self.assertIn(">>>> I'm {}".format(self.reference), self.t.out)
+        self.assertIn(">>>> content: {}".format(self.reference), self.t.out)
+
+        # Change something in the local folder
+        self.new_content = "Updated in the local folder!"
+        self.t.save({'file.txt': self.new_content})
+
+    def test_create_workflow(self):
+        """ Use the 'create' command, local changes are reflected in the cache """
+        self.t.run("create . {}".format(self.reference))
+        self.assertIn(">>>> I'm {}".format(self.reference), self.t.out)
+        self.assertIn(">>>> content: {}".format(self.new_content), self.t.out)
+
+    def test_install_workflow(self):
+        """ Using the install command, it won't be taken into account """
+        t2 = TestClient(base_folder=self.t.base_folder)
+        t2.save({'conanfile.txt': "[requires]\n{}".format(self.reference)})
+        ref = ConanFileReference.loads(self.reference)
+        t2.run("install . --build={}".format(ref.name))
+        self.assertNotIn(self.new_content, t2.out)
+        self.assertIn(">>>> I'm {}".format(self.reference), self.t.out)
+        self.assertIn(">>>> content: {}".format(self.reference), self.t.out)
diff --git a/conans/test/unittests/client/build/cmake_test.py b/conans/test/unittests/client/build/cmake_test.py
index 5bd377007..635607dcb 100644
--- a/conans/test/unittests/client/build/cmake_test.py
+++ b/conans/test/unittests/client/build/cmake_test.py
@@ -1161,6 +1161,32 @@ build_type: [ Release]
             cmake.build()
             self.assertTrue(vcvars_mock.called, "vcvars weren't called")
 
+    def test_cmake_program(self):
+        conanfile = ConanFileMock()
+        settings = Settings.loads(default_settings_yml)
+        settings.os = "Windows"
+        settings.compiler = "Visual Studio"
+        settings.compiler.version = "14"
+        conanfile.settings = settings
+
+        cmake = CMake(conanfile, parallel=False)
+        cmake.build()
+        self.assertEqual("cmake --build %s" % CMakeTest.scape("."), conanfile.command)
+
+        cmake = CMake(conanfile, cmake_program="use_another_cmake", parallel=False)
+        cmake.build()
+        self.assertEqual("use_another_cmake --build %s" % CMakeTest.scape("."), conanfile.command)
+
+        with tools.environment_append({"CONAN_CMAKE_PROGRAM": "my_custom_cmake"}):
+            cmake = CMake(conanfile, parallel=False)
+            cmake.build()
+            self.assertEqual("my_custom_cmake --build %s"  % CMakeTest.scape("."), conanfile.command)
+
+        with tools.environment_append({"CONAN_CMAKE_PROGRAM": "cmake_from_environment_has_priority"}):
+            cmake = CMake(conanfile, cmake_program="but_not_cmake_from_the_ctor", parallel=False)
+            cmake.build()
+            self.assertEqual("cmake_from_environment_has_priority --build %s" % CMakeTest.scape("."), conanfile.command)
+
     def test_msbuild_verbosity(self):
         settings = Settings.loads(default_settings_yml)
         settings.os = "Windows"
@@ -1204,3 +1230,4 @@ build_type: [ Release]
             cmake.test(output_on_failure=True)
             self.assertEquals(conanfile.captured_env["CTEST_OUTPUT_ON_FAILURE"], "1")
             self.assertEquals(conanfile.captured_env["CTEST_PARALLEL_LEVEL"], "666")
+
diff --git a/conans/test/unittests/client/conf/detect/__init__.py b/conans/test/unittests/client/conf/detect/__init__.py
new file mode 100644
index 000000000..992ec4875
--- /dev/null
+++ b/conans/test/unittests/client/conf/detect/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/conans/test/unittests/client/conf/detect/detected_os_test.py b/conans/test/unittests/client/conf/detect/detected_os_test.py
new file mode 100644
index 000000000..e194f7cfb
--- /dev/null
+++ b/conans/test/unittests/client/conf/detect/detected_os_test.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+import unittest
+from mock import mock
+from conans.client.conf.detect import detected_os
+from conans.client.tools import OSInfo
+
+
+class DetectedOSTest(unittest.TestCase):
+    def test_windows(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='Windows')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertEqual(detected_os(), "Windows")
+
+    def test_cygwin(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='CYGWIN_NT-10.0')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertEqual(detected_os(), "Windows")
+
+    def test_msys(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='MSYS_NT-10.0')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertEqual(detected_os(), "Windows")
+
+    def test_mingw32(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='MINGW32_NT-10.0')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertEqual(detected_os(), "Windows")
+
+    def test_mingw64(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='MINGW64_NT-10.0')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertEqual(detected_os(), "Windows")
+
+    def test_darwin(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='Darwin')):
+            self.assertEqual(detected_os(), "Macos")
+
+    def test_linux(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='Linux')):
+            with mock.patch.object(OSInfo, '_get_linux_distro_info'):
+                self.assertEqual(detected_os(), "Linux")
+
+    def test_freebsd(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='FreeBSD')):
+            self.assertEqual(detected_os(), "FreeBSD")
+
+    def test_solaris(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='SunOS')):
+            self.assertEqual(detected_os(), "SunOS")
diff --git a/conans/test/unittests/client/tools/os_info/__init__.py b/conans/test/unittests/client/tools/os_info/__init__.py
new file mode 100644
index 000000000..992ec4875
--- /dev/null
+++ b/conans/test/unittests/client/tools/os_info/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/conans/test/unittests/client/tools/os_info/osinfo_test.py b/conans/test/unittests/client/tools/os_info/osinfo_test.py
new file mode 100644
index 000000000..c49f963f7
--- /dev/null
+++ b/conans/test/unittests/client/tools/os_info/osinfo_test.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+import unittest
+import platform
+import os
+from conans.client.tools import OSInfo, environment_append, CYGWIN, MSYS2, MSYS, remove_from_path
+from conans.errors import ConanException
+from mock import mock
+from nose.plugins.attrib import attr
+
+
+class OSInfoTest(unittest.TestCase):
+
+    def setUp(self):
+        self._uname = None
+        self._version = None
+
+    def subprocess_check_output_mock(self, cmd, shell):
+        if cmd.endswith('"uname"'):
+            return self._uname.encode()
+        elif cmd.endswith('"uname -or"'):
+            return self._version.encode()
+        raise ValueError("don't know how to respond to %s" % cmd)
+
+    def test_windows(self):
+        self._uname = None
+        self._version = None
+        with mock.patch("platform.system", mock.MagicMock(return_value='Windows')):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    with remove_from_path("uname"):
+                        with remove_from_path("bash"):
+                            # FIXME: bug in environment_append removing variables which aren't exist (another PR)
+                            new_env = dict()
+                            if 'CONAN_BASH_PATH' in os.environ:
+                                new_env['CONAN_BASH_PATH'] = None
+                            with environment_append(new_env):
+                                self.assertTrue(OSInfo().is_windows)
+                                self.assertFalse(OSInfo().is_cygwin)
+                                self.assertFalse(OSInfo().is_msys)
+                                self.assertFalse(OSInfo().is_linux)
+                                self.assertFalse(OSInfo().is_freebsd)
+                                self.assertFalse(OSInfo().is_macos)
+                                self.assertFalse(OSInfo().is_solaris)
+
+                                with self.assertRaises(ConanException):
+                                    OSInfo.uname()
+                                self.assertIsNone(OSInfo.detect_windows_subsystem())
+
+    def test_cygwin(self):
+        self._uname = 'CYGWIN_NT-10.0'
+        self._version = '2.11.2(0.329/5/3)'
+        with mock.patch("platform.system", mock.MagicMock(return_value=self._uname)):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertTrue(OSInfo().is_windows)
+                    self.assertTrue(OSInfo().is_cygwin)
+                    self.assertFalse(OSInfo().is_msys)
+                    self.assertFalse(OSInfo().is_linux)
+                    self.assertFalse(OSInfo().is_freebsd)
+                    self.assertFalse(OSInfo().is_macos)
+                    self.assertFalse(OSInfo().is_solaris)
+
+                    with environment_append({"CONAN_BASH_PATH": "/fake/bash.exe"}):
+                        with mock.patch('subprocess.check_output', new=self.subprocess_check_output_mock):
+                            self.assertEqual(OSInfo.uname(), self._uname.lower())
+                            self.assertEqual(OSInfo.detect_windows_subsystem(), CYGWIN)
+
+    def test_msys2(self):
+        self._uname = 'MSYS_NT-10.0'
+        self._version = '1.0.18(0.48/3/2)'
+        with mock.patch("platform.system", mock.MagicMock(return_value=self._uname)):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertTrue(OSInfo().is_windows)
+                    self.assertFalse(OSInfo().is_cygwin)
+                    self.assertTrue(OSInfo().is_msys)
+                    self.assertFalse(OSInfo().is_linux)
+                    self.assertFalse(OSInfo().is_freebsd)
+                    self.assertFalse(OSInfo().is_macos)
+                    self.assertFalse(OSInfo().is_solaris)
+
+                    with environment_append({"CONAN_BASH_PATH": "/fake/bash.exe"}):
+                        with mock.patch('subprocess.check_output', new=self.subprocess_check_output_mock):
+                            self.assertEqual(OSInfo.uname(), self._uname.lower())
+                            self.assertEqual(OSInfo.detect_windows_subsystem(), MSYS)
+
+    def test_mingw32(self):
+        self._uname = 'MINGW32_NT-10.0'
+        self._version = '2.10.0(0.325/5/3)'
+        with mock.patch("platform.system", mock.MagicMock(return_value=self._uname)):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertTrue(OSInfo().is_windows)
+                    self.assertFalse(OSInfo().is_cygwin)
+                    self.assertTrue(OSInfo().is_msys)
+                    self.assertFalse(OSInfo().is_linux)
+                    self.assertFalse(OSInfo().is_freebsd)
+                    self.assertFalse(OSInfo().is_macos)
+                    self.assertFalse(OSInfo().is_solaris)
+
+                    with environment_append({"CONAN_BASH_PATH": "/fake/bash.exe"}):
+                        with mock.patch('subprocess.check_output', new=self.subprocess_check_output_mock):
+                            self.assertEqual(OSInfo.uname(), self._uname.lower())
+                            self.assertEqual(OSInfo.detect_windows_subsystem(), MSYS2)
+
+    def test_mingw64(self):
+        self._uname = 'MINGW64_NT-10.0'
+        self._version = '2.4.0(0.292/5/3)'
+        with mock.patch("platform.system", mock.MagicMock(return_value=self._uname)):
+            with mock.patch.object(OSInfo, "get_win_version_name", return_value="Windows 98"):
+                with mock.patch.object(OSInfo, "get_win_os_version", return_value="4.0"):
+                    self.assertTrue(OSInfo().is_windows)
+                    self.assertFalse(OSInfo().is_cygwin)
+                    self.assertTrue(OSInfo().is_msys)
+                    self.assertFalse(OSInfo().is_linux)
+                    self.assertFalse(OSInfo().is_freebsd)
+                    self.assertFalse(OSInfo().is_macos)
+                    self.assertFalse(OSInfo().is_solaris)
+
+                    with environment_append({"CONAN_BASH_PATH": "/fake/bash.exe"}):
+                        with mock.patch('subprocess.check_output', new=self.subprocess_check_output_mock):
+                            self.assertEqual(OSInfo.uname(), self._uname.lower())
+                            self.assertEqual(OSInfo.detect_windows_subsystem(), MSYS2)
+
+    def test_linux(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='Linux')):
+            with mock.patch.object(OSInfo, '_get_linux_distro_info'):
+                self.assertFalse(OSInfo().is_windows)
+                self.assertFalse(OSInfo().is_cygwin)
+                self.assertFalse(OSInfo().is_msys)
+                self.assertTrue(OSInfo().is_linux)
+                self.assertFalse(OSInfo().is_freebsd)
+                self.assertFalse(OSInfo().is_macos)
+                self.assertFalse(OSInfo().is_solaris)
+
+                with self.assertRaises(ConanException):
+                    OSInfo.uname()
+                self.assertIsNone(OSInfo.detect_windows_subsystem())
+
+    def test_macos(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='Darwin')):
+            self.assertFalse(OSInfo().is_windows)
+            self.assertFalse(OSInfo().is_cygwin)
+            self.assertFalse(OSInfo().is_msys)
+            self.assertFalse(OSInfo().is_linux)
+            self.assertFalse(OSInfo().is_freebsd)
+            self.assertTrue(OSInfo().is_macos)
+            self.assertFalse(OSInfo().is_solaris)
+
+            with self.assertRaises(ConanException):
+                OSInfo.uname()
+            self.assertIsNone(OSInfo.detect_windows_subsystem())
+
+    def test_freebsd(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='FreeBSD')):
+            self.assertFalse(OSInfo().is_windows)
+            self.assertFalse(OSInfo().is_cygwin)
+            self.assertFalse(OSInfo().is_msys)
+            self.assertFalse(OSInfo().is_linux)
+            self.assertTrue(OSInfo().is_freebsd)
+            self.assertFalse(OSInfo().is_macos)
+            self.assertFalse(OSInfo().is_solaris)
+
+            with self.assertRaises(ConanException):
+                OSInfo.uname()
+            self.assertIsNone(OSInfo.detect_windows_subsystem())
+
+    def test_solaris(self):
+        with mock.patch("platform.system", mock.MagicMock(return_value='SunOS')):
+            self.assertFalse(OSInfo().is_windows)
+            self.assertFalse(OSInfo().is_cygwin)
+            self.assertFalse(OSInfo().is_msys)
+            self.assertFalse(OSInfo().is_linux)
+            self.assertFalse(OSInfo().is_freebsd)
+            self.assertFalse(OSInfo().is_macos)
+            self.assertTrue(OSInfo().is_solaris)
+
+            with self.assertRaises(ConanException):
+                OSInfo.uname()
+            self.assertIsNone(OSInfo.detect_windows_subsystem())
diff --git a/conans/test/unittests/client/tools/test_env.py b/conans/test/unittests/client/tools/test_env.py
index 2fcc1c1c8..556139e30 100644
--- a/conans/test/unittests/client/tools/test_env.py
+++ b/conans/test/unittests/client/tools/test_env.py
@@ -46,3 +46,9 @@ class ToolsEnvTest(unittest.TestCase):
                               'env_var2': 'value2'}),\
              env.environment_append({'env_var1': None}):
                 self.assertNotIn('env_var1', os.environ)
+
+    def test_environment_append_unsetting_non_existing_variables(self):
+        with mock.patch.dict('os.environ',
+                             {'env_var2': 'value2'}),\
+             env.environment_append({'env_var1': None}):
+                self.assertNotIn('env_var1', os.environ)