sublime-config: Change GOPATH/GOROOT checker to raise exceptions

When checking GOROOT and GOPATH to ensure they exist on disk, raise
an exception if they are not found. This allows consuming packages
to present the user with more information about why the environment
variable value could not be found.

This change also syncs the changelog and in-code version numbers
with the tags that have been used so far.

Change-Id: I00ee05bde751848737d0346193444cece5074089
Reviewed-on: https://go-review.googlesource.com/16570
Reviewed-by: Jason Buberel <jbuberel@google.com>
diff --git a/all/golangconfig.py b/all/golangconfig.py
index cf57e0f..24d075f 100644
--- a/all/golangconfig.py
+++ b/all/golangconfig.py
@@ -15,8 +15,8 @@
     py2 = False
 
 
-__version__ = '1.0.0-beta'
-__version_info__ = (1, 0, 0, 'beta')
+__version__ = '0.9.0'
+__version_info__ = (0, 9, 0)
 
 
 # The sublime.platform() function will not be available in ST3 upon initial
@@ -45,6 +45,24 @@
     missing = None
 
 
+class GoRootNotFoundError(EnvironmentError):
+
+    """
+    An error occurred finding the $GOROOT on disk
+    """
+
+    directory = None
+
+
+class GoPathNotFoundError(EnvironmentError):
+
+    """
+    An error occurred finding one or more directories from $GOPATH on disk
+    """
+
+    directories = None
+
+
 class ExecutableError(EnvironmentError):
 
     """
@@ -119,6 +137,19 @@
             When one or more required_vars are not available. The .missing
             attribute will be a list of the names of missing environment
             variables.
+        golangconfig.GoPathNotFoundError
+            When one or more directories specified by the GOPATH environment
+            variable could not be found on disk. The .directories attribute will
+            be a list of the directories that could not be found.
+        golangconfig.GoRootNotFoundError
+            When the directory specified by GOROOT environment variable could
+            not be found on disk. The .directory attribute will be the path to
+            the directory that could not be found.
+
+        golangconfig.EnvVarError
+            When one or more required_vars are not available. The .missing
+            attribute will be a list of the names of missing environment
+            variables.
 
     :return:
         A two-element tuple.
@@ -245,6 +276,14 @@
             When the function is called from any thread but the UI thread
         TypeError
             When any of the parameters are of the wrong type
+        golangconfig.GoPathNotFoundError
+            When one or more directories specified by the GOPATH environment
+            variable could not be found on disk. The .directories attribute will
+            be a list of the directories that could not be found.
+        golangconfig.GoRootNotFoundError
+            When the directory specified by GOROOT environment variable could
+            not be found on disk. The .directory attribute will be the path to
+            the directory that could not be found.
 
     :return:
         A two-element tuple.
@@ -285,48 +324,51 @@
     if setting_name not in set(['GOPATH', 'GOROOT']):
         return (setting, source)
 
+    if setting is None and source is None:
+        return (setting, source)
+
     # We add some extra processing here for known settings to improve the
     # user experience, especially around debugging
-    is_str = isinstance(setting, str_cls)
-    if is_str:
+    _debug_unicode_string(setting_name, setting, source)
 
-        multiple_values = False
+    if not isinstance(setting, str_cls):
+        setting = str_cls(setting)
 
-        if setting_name == 'GOROOT':
-            if os.path.exists(setting):
-                return (setting, source)
+    if setting_name == 'GOROOT':
+        if os.path.exists(setting):
+            return (setting, source)
 
-        if setting_name == 'GOPATH':
-            values = setting.split(os.pathsep)
-            multiple_values = len(values) > 1
-            missing = False
+    has_multiple = False
+    if setting_name == 'GOPATH':
+        values = setting.split(os.pathsep)
+        has_multiple = len(values) > 1
+        missing = []
 
-            for value in values:
-                if not os.path.exists(value):
-                    missing = True
-                    break
+        for value in values:
+            if not os.path.exists(value):
+                missing.append(value)
 
-            if not missing:
-                return (setting, source)
+        if not missing:
+            return (setting, source)
 
-        if multiple_values:
-            value_desc = "one of the values"
-        else:
-            value_desc = "the value"
-        print(
-            'golangconfig: %s for %s from %s - "%s" - does not exist on the filesystem' %
-            (
-                value_desc,
-                setting_name,
-                source,
-                setting
-            )
-        )
+    if setting_name == 'GOROOT':
+        message = 'The GOROOT environment variable value "%s" does not exist on the filesystem'
+        e = GoRootNotFoundError(message % setting)
+        e.directory = setting
+        raise e
 
-    elif debug_enabled():
-        _debug_unicode_string(setting_name, setting, source)
+    if not has_multiple:
+        suffix = 'value "%s" does not exist on the filesystem' % missing[0]
+    elif len(missing) == 1:
+        suffix = 'contains the directory "%s" that does not exist on the filesystem' % missing[0]
+    else:
+        paths = ', '.join('"' + path + '"' for path in missing)
+        suffix = 'contains %s directories that do not exist on the filesystem: %s' % (len(missing), paths)
 
-    return (None, None)
+    message = 'The GOPATH environment variable ' + suffix
+    e = GoPathNotFoundError(message)
+    e.directories = missing
+    raise e
 
 
 def executable_path(executable_name, view=None, window=None):
diff --git a/dev/tests.py b/dev/tests.py
index 4d3cab4..d2344db 100644
--- a/dev/tests.py
+++ b/dev/tests.py
@@ -186,7 +186,7 @@
                 'go',
                 ['GOPATH', 'GOROOT'],
                 None,
-                golangconfig.EnvVarError
+                golangconfig.GoRootNotFoundError
             ),
         )
 
@@ -533,11 +533,9 @@
             'GOPATH': os.path.join(os.path.expanduser('~'), 'hdjsahkjzhkjzhiashs7hdsuybyusbguycas')
         }
         with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context:
-            self.assertEquals(
-                (None, None),
+            def do_test():
                 golangconfig.setting_value('GOPATH', mock_context.view, mock_context.window)
-            )
-            self.assertTrue('does not exist on the filesystem' in sys.stdout.getvalue())
+            self.assertRaises(golangconfig.GoPathNotFoundError, do_test)
 
     def test_setting_value_multiple_gopath_one_not_existing(self):
         shell = '/bin/bash'
@@ -547,11 +545,10 @@
         with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context:
             mock_context.replace_tempdir_env()
             mock_context.make_dirs(['usr/bin'])
-            self.assertEquals(
-                (None, None),
+
+            def do_test():
                 golangconfig.setting_value('GOPATH', mock_context.view, mock_context.window)
-            )
-            self.assertTrue('one of the values for GOPATH' in sys.stdout.getvalue())
+            self.assertRaises(golangconfig.GoPathNotFoundError, do_test)
 
     def test_setting_value_multiple_gopath(self):
         shell = '/bin/bash'
@@ -573,11 +570,9 @@
             'GOPATH': 1
         }
         with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context:
-            self.assertEquals(
-                (None, None),
+            def do_test():
                 golangconfig.setting_value('GOPATH', mock_context.view, mock_context.window)
-            )
-            self.assertTrue('is not a string' in sys.stdout.getvalue())
+            self.assertRaises(golangconfig.GoPathNotFoundError, do_test)
 
     def test_subprocess_info_goroot_executable_not_inside(self):
         shell = '/bin/bash'
diff --git a/docs/changelog.md b/docs/changelog.md
index 015d74e..107a04d 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,18 @@
 # golangconfig Changelog
 
-## 1.0.0
+## 0.9.0
+
+ - `subprocess_info()` and `setting_value()` will now raise
+   `GoRootNotFoundError()` when the `GOROOT` environment variable points to a
+   directory that could not be found on disk, and `GoPathNotFoundError()` when
+   one or more of the entries in the `GOPATH` environment variable could not be
+   found on disk.
+
+## 0.8.1
+
+ - Added support for GOPATH with multiple directories
+ - Fix an error when no project was open when using Sublime Text 3
+
+## 0.8.0
 
  - Initial release
diff --git a/docs/package_developer.md b/docs/package_developer.md
index faef4bb..0700132 100644
--- a/docs/package_developer.md
+++ b/docs/package_developer.md
@@ -84,6 +84,15 @@
 which is a list of all required environment variables that could not be
 found in the Sublime Text settings, or the shell environment.
 
+If the `GOROOT` environment variable points to a directory that does not exist
+on disk, the `golangconfig.GoRootNotFoundError()` will be raised. It has one
+attribute `.directory` that contains a unicode string of the `GOROOT` value.
+
+If one or more of the directories specified in the `GOPATH` environment variable
+can not be found on disk, the `golangconfig.GoPathNotFoundError()` will be
+raised. It has one attribute `.directories`, which is a list on unicode strings
+of the directories that could not be found.
+
 ### Requiring the Dependency
 
 When developing a package to utilize `golangconfig`, Package Control needs to be
@@ -116,11 +125,18 @@
 # coding: utf-8
 from __future__ import unicode_literals
 
+import sys
+
 import sublime
 import sublime_plugin
 
 import golangconfig
 
+if sys.version_info < (3,):
+    str_cls = unicode
+else:
+    str_cls = str
+
 
 class MyWindowCommand(sublime_plugin.WindowCommand):
     def run(self):
@@ -183,6 +199,23 @@
                     {'url': 'http://example.com/documentation'}
                 )
 
+        except (golangconfig.GoRootNotFoundError, golangconfig.GoPathNotFoundError) as e:
+            error_message = '''
+                My Package
+
+                %s.
+
+                Would you like to view the configuration documentation?
+            '''
+
+            prompt = error_message % str_cls(e)
+
+            if sublime.ok_cancel_dialog(prompt, 'Open Documentation'):
+                self.window.run_command(
+                    'open_url',
+                    {'url': 'http://example.com/documentation'}
+                )
+
 
 class MyTextCommand(sublime_plugin.TextCommand):
     def run(self):