error: fix pickling of all exceptions

Make sure all our custom exceptions can be pickled so that if they
get thrown in a multiprocess subprocess, we don't crash & hang due
to multiprocessing being unable to pickle+unpickle the exception.

Details/examples can be seen in Python reports like:
https://bugs.python.org/issue13751

Change-Id: Iddf14d3952ad4e2867cfc71891d6b6559130df4b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/297382
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
diff --git a/error.py b/error.py
index 8bb64b8..20d5f72 100644
--- a/error.py
+++ b/error.py
@@ -37,7 +37,7 @@
   """
 
   def __init__(self, path, reason):
-    super(NoManifestException, self).__init__()
+    super(NoManifestException, self).__init__(path, reason)
     self.path = path
     self.reason = reason
 
@@ -50,7 +50,7 @@
   """
 
   def __init__(self, reason):
-    super(EditorError, self).__init__()
+    super(EditorError, self).__init__(reason)
     self.reason = reason
 
   def __str__(self):
@@ -62,7 +62,7 @@
   """
 
   def __init__(self, command):
-    super(GitError, self).__init__()
+    super(GitError, self).__init__(command)
     self.command = command
 
   def __str__(self):
@@ -74,7 +74,7 @@
   """
 
   def __init__(self, reason):
-    super(UploadError, self).__init__()
+    super(UploadError, self).__init__(reason)
     self.reason = reason
 
   def __str__(self):
@@ -86,7 +86,7 @@
   """
 
   def __init__(self, reason):
-    super(DownloadError, self).__init__()
+    super(DownloadError, self).__init__(reason)
     self.reason = reason
 
   def __str__(self):
@@ -98,7 +98,7 @@
   """
 
   def __init__(self, name=None):
-    super(NoSuchProjectError, self).__init__()
+    super(NoSuchProjectError, self).__init__(name)
     self.name = name
 
   def __str__(self):
@@ -112,7 +112,7 @@
   """
 
   def __init__(self, name=None):
-    super(InvalidProjectGroupsError, self).__init__()
+    super(InvalidProjectGroupsError, self).__init__(name)
     self.name = name
 
   def __str__(self):
@@ -128,7 +128,7 @@
   """
 
   def __init__(self, extra_args=None):
-    super(RepoChangedException, self).__init__()
+    super(RepoChangedException, self).__init__(extra_args)
     self.extra_args = extra_args or []
 
 
diff --git a/tests/test_error.py b/tests/test_error.py
new file mode 100644
index 0000000..82b00c2
--- /dev/null
+++ b/tests/test_error.py
@@ -0,0 +1,53 @@
+# Copyright 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for the error.py module."""
+
+import inspect
+import pickle
+import unittest
+
+import error
+
+
+class PickleTests(unittest.TestCase):
+  """Make sure all our custom exceptions can be pickled."""
+
+  def getExceptions(self):
+    """Return all our custom exceptions."""
+    for name in dir(error):
+      cls = getattr(error, name)
+      if isinstance(cls, type) and issubclass(cls, Exception):
+        yield cls
+
+  def testExceptionLookup(self):
+    """Make sure our introspection logic works."""
+    classes = list(self.getExceptions())
+    self.assertIn(error.HookError, classes)
+    # Don't assert the exact number to avoid being a change-detector test.
+    self.assertGreater(len(classes), 10)
+
+  def testPickle(self):
+    """Try to pickle all the exceptions."""
+    for cls in self.getExceptions():
+      args = inspect.getfullargspec(cls.__init__).args[1:]
+      obj = cls(*args)
+      p = pickle.dumps(obj)
+      try:
+        newobj = pickle.loads(p)
+      except Exception as e:  # pylint: disable=broad-except
+        self.fail('Class %s is unable to be pickled: %s\n'
+                  'Incomplete super().__init__(...) call?' % (cls, e))
+      self.assertIsInstance(newobj, cls)
+      self.assertEqual(str(obj), str(newobj))