Repository: SCons/scons

Issue:
Usage of non-existent error types used in CacheDir
* Version of SCons: 3.1.0
* Version of Python: 3.7.0
* Which python distribution if applicable (python.org, cygwin, anaconda, macports, brew,etc): Custom compiled Linux Python
* How you installed SCons: scons-local
* What Platform are you on? (Linux/Windows and which version): Linux Ubuntu 18.04

When hitting some `CacheDir` error cases it should raise a `SConsEnvironmentError` but instead uses the older `EnvironmentError` which conflicts with the Python standard library. This causes the actual error that CacheDir encountered to be obscured.

An example failure of one of these obfuscated failures:

```
[2019/07/21 10:41:55.644] AttributeError: module 'SCons.Errors' has no attribute 'EnvironmentError':
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/SConstruct", line 3632:
[2019/07/21 10:41:55.644]     env = doConfigure( env )
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/SConstruct", line 2081:
[2019/07/21 10:41:55.644]     c_compiler_validated = conf.CheckForMinimumCompiler('C')
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/SConf.py", line 691:
[2019/07/21 10:41:55.644]     ret = self.test(context, *args, **kw)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/SConstruct", line 2073:
[2019/07/21 10:41:55.644]     result = context.TryCompile(compiler_test_body, extension_for[language])
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/SConf.py", line 860:
[2019/07/21 10:41:55.644]     return self.sconf.TryCompile(*args, **kw)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/SConf.py", line 654:
[2019/07/21 10:41:55.644]     return self.TryBuild(self.env.Object, text, extension)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/SConf.py", line 620:
[2019/07/21 10:41:55.644]     result = self.BuildNodes(nodesToBeBuilt)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/SConf.py", line 547:
[2019/07/21 10:41:55.644]     jobs.run()
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/Job.py", line 111:
[2019/07/21 10:41:55.644]     self.job.start()
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/Job.py", line 216:
[2019/07/21 10:41:55.644]     task.executed()
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/Taskmaster.py", line 312:
[2019/07/21 10:41:55.644]     t.push_to_cache()
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/Node/FS.py", line 2967:
[2019/07/21 10:41:55.644]     self.get_build_env().get_CacheDir().push(self)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/Environment.py", line 1025:
[2019/07/21 10:41:55.644]     cd = SCons.CacheDir.CacheDir(path)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/CacheDir.py", line 163:
[2019/07/21 10:41:55.644]     self._readconfig3(path)
[2019/07/21 10:41:55.644]   File "/data/mci/af4fdf71b05da25a8fec1956bdaff896/src/src/third_party/scons-3.1.0/scons-local-3.1.0/SCons/CacheDir.py", line 188:
[2019/07/21 10:41:55.644]     raise SCons.Errors.EnvironmentError(msg)
```


Discussion:

Resolving patch:
diff --git a/src/engine/SCons/CacheDir.py b/src/engine/SCons/CacheDir.py
--- a/src/engine/SCons/CacheDir.py
+++ b/src/engine/SCons/CacheDir.py
@@ -33,6 +33,7 @@
 import stat
 import sys
 
+import SCons
 import SCons.Action
 import SCons.Warnings
 from SCons.Util import PY3
@@ -185,7 +186,7 @@ def _readconfig3(self, path):
             pass
         except OSError:
             msg = "Failed to create cache directory " + path
-            raise SCons.Errors.EnvironmentError(msg)
+            raise SCons.Errors.SConsEnvironmentError(msg)
 
         try:
             with open(config_file, 'x') as config:
@@ -194,14 +195,14 @@ def _readconfig3(self, path):
                     json.dump(self.config, config)
                 except Exception:
                     msg = "Failed to write cache configuration for " + path
-                    raise SCons.Errors.EnvironmentError(msg)
+                    raise SCons.Errors.SConsEnvironmentError(msg)
         except FileExistsError:
             try:
                 with open(config_file) as config:
                     self.config = json.load(config)
             except ValueError:
                 msg = "Failed to read cache configuration for " + path
-                raise SCons.Errors.EnvironmentError(msg)
+                raise SCons.Errors.SConsEnvironmentError(msg)
 
 
     def _readconfig2(self, path):
diff --git a/src/engine/SCons/CacheDirTests.py b/src/engine/SCons/CacheDirTests.py
--- a/src/engine/SCons/CacheDirTests.py
+++ b/src/engine/SCons/CacheDirTests.py
@@ -27,10 +27,13 @@
 import shutil
 import sys
 import unittest
+import tempfile
+import stat
 
 from TestCmd import TestCmd
 
 import SCons.CacheDir
+from SCons.Util import PY3
 
 built_it = None
 
@@ -112,6 +115,84 @@ def my_collect(list):
         finally:
             SCons.Util.MD5collect = save_collect
 
+class ExceptionTestCase(unittest.TestCase):
+    """Test that the correct exceptions are thrown by CacheDir."""
+
+    # Don't inherit from BaseTestCase, we're by definition trying to
+    # break things so we really want a clean slate for each test.
+    def setUp(self):
+        self.tmpdir = tempfile.mkdtemp()
+        self._CacheDir = SCons.CacheDir.CacheDir(self.tmpdir)
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+
+    @unittest.skipIf(sys.platform.startswith("win"), "This fixture will not trigger an OSError on Windows")
+    def test_throws_correct_on_OSError(self):
+        """Test that the correct error is thrown when cache directory cannot be created."""
+        privileged_dir = os.path.join(os.getcwd(), "privileged")
+        try:
+            os.mkdir(privileged_dir)
+            os.chmod(privileged_dir, stat.S_IREAD)
+            cd = SCons.CacheDir.CacheDir(os.path.join(privileged_dir, "cache"))
+            assert False, "Should have raised exception and did not"
+        except SCons.Errors.SConsEnvironmentError as e:
+            assert str(e) == "Failed to create cache directory {}".format(os.path.join(privileged_dir, "cache"))
+        finally:
+            os.chmod(privileged_dir, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
+            shutil.rmtree(privileged_dir)
+
+
+    def test_throws_correct_when_failed_to_write_configfile(self):
+        class Unserializable:
+            """A class which the JSON should not be able to serialize"""
+
+            def __init__(self, oldconfig):
+                self.something = 1 # Make the object unserializable
+                # Pretend to be the old config just enough
+                self.__dict__["prefix_len"] = oldconfig["prefix_len"]
+
+            def __getitem__(self, name, default=None):
+                if name == "prefix_len":
+                    return self.__dict__["prefix_len"]
+                else:
+                    return None
+
+            def __setitem__(self, name, value):
+                self.__dict__[name] = value
+
+        oldconfig = self._CacheDir.config
+        self._CacheDir.config = Unserializable(oldconfig)
+        # Remove the config file that got created on object creation
+        # so that _readconfig* will try to rewrite it
+        old_config = os.path.join(self._CacheDir.path, "config")
+        os.remove(old_config)
+        
+        try:
+            if PY3:
+                self._CacheDir._readconfig3(self._CacheDir.path)
+            else:
+                self._CacheDir._readconfig2(self._CacheDir.path)
+            assert False, "Should have raised exception and did not"
+        except SCons.Errors.SConsEnvironmentError as e:
+            assert str(e) == "Failed to write cache configuration for {}".format(self._CacheDir.path)
+
+    def test_raise_environment_error_on_invalid_json(self):
+        config_file = os.path.join(self._CacheDir.path, "config")
+        with open(config_file, "r") as cfg:
+            content = cfg.read()
+        # This will make JSON load raise a ValueError
+        content += "{}"
+        with open(config_file, "w") as cfg:
+            cfg.write(content)
+
+        try:
+            # Construct a new cache dir that will try to read the invalid config
+            new_cache_dir = SCons.CacheDir.CacheDir(self._CacheDir.path)
+            assert False, "Should have raised exception and did not"
+        except SCons.Errors.SConsEnvironmentError as e:
+            assert str(e) == "Failed to read cache configuration for {}".format(self._CacheDir.path)
+
 class FileTestCase(BaseTestCase):
     """
     Test calling CacheDir code through Node.FS.File interfaces.


Test patch: