Repository: amwa-tv/nmos-testing
Issue: SimpleJSON causes uncaught exceptions

We've hit this before in https://github.com/AMWA-TV/nmos-testing/issues/2

If a system has simplejson installed it will be used in preference to the built in JSON parser. Unfortunately it has different exception names which cause things to fail. I don't have a good solution to this yet (and would have thought it'd be a more common problem with a best practice solution), but we should probably keep an issue open as a reminder for whenever it comes up.

Changed files:
IS0402Test.py, IS0401Test.py, GenericTest.py, BCP00301Test.py, IS0502Test.py, IS0802Test.py, nmos-test.py

diff --git a/BCP00301Test.py b/BCP00301Test.py
index 028ebeb..c0aaad7 100644
--- a/BCP00301Test.py
+++ b/BCP00301Test.py
@@ -14,7 +14,7 @@
 
 import os
 import subprocess
-import json
+from requests.compat import json
 import ssl
 import socket
 import ipaddress
diff --git a/GenericTest.py b/GenericTest.py
index 8d9b48a..15e4463 100644
--- a/GenericTest.py
+++ b/GenericTest.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import os
-import json
+from requests.compat import json
 import git
 import jsonschema
 import TestHelper
@@ -244,7 +244,7 @@ class GenericTest(object):
                     return test.FAIL("Response is not an array containing '{}'".format(expectation))
                 else:
                     return test.PASS()
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned")
 
     def check_response(self, schema, method, response):
@@ -256,7 +256,7 @@ class GenericTest(object):
             self.validate_schema(response.json(), schema)
         except jsonschema.ValidationError:
             return False, "Response schema validation error"
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return False, "Invalid JSON received"
 
         return True, ""
@@ -404,7 +404,7 @@ class GenericTest(object):
                     # Cover the audio channel mapping spec case with dictionary keys
                     if isinstance(key, str) and isinstance(value, dict):
                         subresources.append(key)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             pass
 
         if len(subresources) > 0:
diff --git a/IS0401Test.py b/IS0401Test.py
index c824a5f..00a2f24 100644
--- a/IS0401Test.py
+++ b/IS0401Test.py
@@ -16,7 +16,7 @@
 
 import time
 import socket
-import json
+from requests.compat import json
 from urllib.parse import urlparse
 
 from copy import deepcopy
@@ -674,7 +674,7 @@ class IS0401Test(GenericTest):
 
             if len(formats_tested) > 0:
                 return test.PASS()
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         return test.UNCLEAR("Node API does not expose any Receivers")
@@ -710,7 +710,7 @@ class IS0401Test(GenericTest):
                                          "subscription".format(receiver["id"]))
 
                 return test.PASS()
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         return test.UNCLEAR("Node API does not expose any Receivers")
@@ -792,7 +792,7 @@ class IS0401Test(GenericTest):
             return test.FAIL("Unexpected response from the Node API: {}".format(response))
         try:
             uuids.add(response.json()["id"])
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         for resource_type in ["devices", "sources", "flows", "senders", "receivers"]:
@@ -805,7 +805,7 @@ class IS0401Test(GenericTest):
                         return test.FAIL("Duplicate ID '{}' found in Node API '{}' resource".format(resource["id"],
                                                                                                     resource_type))
                     uuids.add(resource["id"])
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned from Node API")
 
         return test.PASS()
@@ -828,7 +828,7 @@ class IS0401Test(GenericTest):
                     "senders": set(resource["senders"]),
                     "receivers": set(resource["receivers"])
                 }
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         if len(from_devices) == 0:
@@ -846,7 +846,7 @@ class IS0401Test(GenericTest):
                     if id not in to_devices:
                         to_devices[id] = deepcopy(empty_refs)
                     to_devices[id][resource_type].add(resource["id"])
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned from Node API")
 
         found_empty_refs = False
@@ -897,7 +897,7 @@ class IS0401Test(GenericTest):
                 if clock_name in clocks:
                     return test.FAIL("Duplicate clock name '{}' found in Node API self resource".format(clock_name))
                 clocks.add(clock_name)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         valid, response = self.do_request("GET", self.node_url + "sources")
@@ -908,7 +908,7 @@ class IS0401Test(GenericTest):
                 clock_name = source["clock_name"]
                 if clock_name not in clocks and clock_name is not None:
                     return test.FAIL("Source '{}' uses a non-existent clock name '{}'".format(source["id"], clock_name))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         return test.PASS()
@@ -931,7 +931,7 @@ class IS0401Test(GenericTest):
                     return test.FAIL("Duplicate interface name '{}' found in Node API self resource"
                                      .format(interface_name))
                 interfaces.add(interface_name)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         valid, response = self.do_request("GET", self.node_url + "senders")
@@ -944,7 +944,7 @@ class IS0401Test(GenericTest):
                     if interface_name not in interfaces:
                         return test.FAIL("Sender '{}' uses a non-existent interface name '{}'"
                                          .format(sender["id"], interface_name))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         valid, response = self.do_request("GET", self.node_url + "receivers")
@@ -957,7 +957,7 @@ class IS0401Test(GenericTest):
                     if interface_name not in interfaces:
                         return test.FAIL("Receiver '{}' uses a non-existent interface name '{}'"
                                          .format(receiver["id"], interface_name))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         return test.PASS()
@@ -1006,7 +1006,7 @@ class IS0401Test(GenericTest):
                     service_href_scheme_warn = True
                 if href.startswith("https://") and urlparse(href).hostname[-1].isdigit():
                     service_href_hostname_warn = True
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         if not found_api_endpoint:
@@ -1030,7 +1030,7 @@ class IS0401Test(GenericTest):
                             control_href_scheme_warn = True
                         if href.startswith("https://") and urlparse(href).hostname[-1].isdigit():
                             control_href_hostname_warn = True
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned from Node API")
 
         valid, response = self.do_request("GET", self.node_url + "senders")
@@ -1044,7 +1044,7 @@ class IS0401Test(GenericTest):
                     manifest_href_scheme_warn = True
                 if href.startswith("https://") and urlparse(href).hostname[-1].isdigit():
                     manifest_href_hostname_warn = True
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
 
         if href_hostname_warn:
@@ -1110,7 +1110,7 @@ class IS0401Test(GenericTest):
                 if not found_delete:
                     return test.FAIL("Node did not attempt to DELETE itself having encountered a 200 code on initial "
                                      "registration")
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned from Node API")
         else:
             return test.FAIL("Unexpected responses from Node API self resource")
diff --git a/IS0402Test.py b/IS0402Test.py
index 7035577..6e1e1db 100644
--- a/IS0402Test.py
+++ b/IS0402Test.py
@@ -17,7 +17,7 @@
 from time import sleep
 import socket
 import uuid
-import json
+from requests.compat import json
 from copy import deepcopy
 from jsonschema import ValidationError
 import re
@@ -419,7 +419,7 @@ class IS0402Test(GenericTest):
                         raise NMOSTestException(test.FAIL("Query API response did not include the correct resources, "
                                                           "for query: {}".format(query_string)))
 
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 raise NMOSTestException(test.FAIL("Non-JSON response returned"))
             except KeyError:
                 raise NMOSTestException(test.FAIL("Query API did not respond as expected, "
@@ -928,7 +928,7 @@ class IS0402Test(GenericTest):
                 return test.FAIL("Registration API failed to respond to request")
             else:
                 reg_versions = [version.rstrip("/") for version in r.json()]
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         # Sort the list and remove API versions higher than the one under test
@@ -944,7 +944,7 @@ class IS0402Test(GenericTest):
                 return test.FAIL("Query API failed to respond to request")
             else:
                 query_versions = [version.rstrip("/") for version in r.json()]
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         # Sort the list and remove API versions higher than the one under test
@@ -1034,7 +1034,7 @@ class IS0402Test(GenericTest):
                     if len(expected_nodes) > 0:
                         return test.FAIL("Query API failed to expose an expected Node when downgrading to {}"
                                          .format(api_version))
-            except json.decoder.JSONDecodeError:
+            except json.JSONDecodeError:
                 return test.FAIL("Non-JSON response returned")
 
         return test.PASS()
@@ -1053,7 +1053,7 @@ class IS0402Test(GenericTest):
                 return test.FAIL("Query API failed to respond to request")
             else:
                 query_versions = [version.rstrip("/") for version in r.json()]
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         # Sort the list and remove API versions higher than the one under test
@@ -1093,7 +1093,7 @@ class IS0402Test(GenericTest):
                 try:
                     for subscription in r.json():
                         subscription_ids.add(subscription["id"])
-                except json.decoder.JSONDecodeError:
+                except json.JSONDecodeError:
                     return test.FAIL("Non-JSON response returned")
 
         if valid_sub_id not in subscription_ids:
@@ -1143,7 +1143,7 @@ class IS0402Test(GenericTest):
             elif len(r.json()) != 1:
                 return test.FAIL("Query API returned {} records for query {} when 1 was expected"
                                  .format(len(r.json()), query_string))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         return test.PASS()
@@ -1162,7 +1162,7 @@ class IS0402Test(GenericTest):
                 return test.OPTIONAL("Query API signalled that it does not support basic queries. This may be "
                                      "important for scalability.",
                                      NMOS_WIKI_URL + "/IS-04#registries-basic-queries")
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         # Create subscription to a specific Node description
@@ -1310,7 +1310,7 @@ class IS0402Test(GenericTest):
             elif len(r.json()) != 1:
                 return test.FAIL("Query API returned {} records for query {} when 1 was expected"
                                  .format(len(r.json()), query_string))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         return test.PASS()
@@ -1330,7 +1330,7 @@ class IS0402Test(GenericTest):
                 return test.OPTIONAL("Query API signalled that it does not support RQL queries. This may be "
                                      "important for scalability.",
                                      NMOS_WIKI_URL + "/IS-04#registries-resource-query-language-rql")
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         # Create subscription to a specific Node description
@@ -1467,7 +1467,7 @@ class IS0402Test(GenericTest):
                                                   "{} {}".format(r.status_code, r.text)))
             elif len(r.json()) > 0:
                 return test.FAIL("Query API returned more records than expected for query: {}".format(query_string))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned")
 
         return test.PASS()
@@ -2083,7 +2083,7 @@ class IS0402Test(GenericTest):
             else:
                 raise NMOSTestException(test.FAIL("Cannot request websocket subscription. Cannot execute test: {}"
                                                   .format(r.status_code)))
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             raise NMOSTestException(test.FAIL("Non-JSON response returned for Query API subscription request"))
 
     def post_resource(self, test, type, data=None, reg_url=None, codes=None, fail=Test.FAIL):
diff --git a/IS0502Test.py b/IS0502Test.py
index 7b7476f..f27e7ec 100644
--- a/IS0502Test.py
+++ b/IS0502Test.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import json
+from requests.compat import json
 import time
 import uuid
 
@@ -56,7 +56,7 @@ class IS0502Test(GenericTest):
             for resource in resources.json():
                 self.is04_resources[resource_type].append(resource)
             self.is04_resources["_requested"].append(resource_type)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return False, "Non-JSON response returned from Node API"
 
         return True, ""
@@ -85,7 +85,7 @@ class IS0502Test(GenericTest):
             for resource in resources.json():
                 self.is05_resources[resource_type].append(resource.rstrip("/"))
             self.is05_resources["_requested"].append(resource_type)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return False, "Non-JSON response returned from Node API"
 
         return True, ""
@@ -150,7 +150,7 @@ class IS0502Test(GenericTest):
                 if not found_04_resource:
                     return False, "Unable to find an IS-04 resource with ID {}".format(is05_resource)
 
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return False, "Non-JSON response returned from Node API"
         except KeyError:
             return False, "Version attribute was not found in IS-04 resource"
@@ -280,7 +280,7 @@ class IS0502Test(GenericTest):
                         is05_devices.append(control["href"])
                         if self.is05_utils.compare_urls(self.connection_url, control["href"]):
                             found_api_match = True
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Node API")
         except KeyError:
             return test.FAIL("One or more Devices were missing the 'controls' attribute")
@@ -533,7 +533,7 @@ class IS0502Test(GenericTest):
                     if trans_params_length != bindings_length:
                         return test.FAIL("Array length mismatch for Sender/Receiver ID '{}'".format(resource["id"]))
 
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return test.FAIL("Non-JSON response returned from Connection API")
         except KeyError as ex:
             return test.FAIL("Expected attribute not found in IS-04 Sender/Receiver \\
diff --git a/IS0802Test.py b/IS0802Test.py
index 62b9abe..63d00a8 100644
--- a/IS0802Test.py
+++ b/IS0802Test.py
@@ -19,7 +19,7 @@ from is08.activation import Activation
 from is08.outputs import getOutputList
 from is08.inputs import getInputList
 from NMOSUtils import NMOSUtils
-import json
+from requests.compat import json
 
 MAPPING_API_KEY = "channelmapping"
 NODE_API_KEY = "node"
@@ -143,7 +143,7 @@ class IS0802Test(GenericTest):
             for resource in resources.json():
                 self.is04_resources[resource_type].append(resource)
             self.is04_resources["_requested"].append(resource_type)
-        except json.decoder.JSONDecodeError:
+        except json.JSONDecodeError:
             return False, "Non-JSON response returned from Node API"
 
         return True, ""
diff --git a/nmos-test.py b/nmos-test.py
index 19ec6cf..6dc4ef4 100644
--- a/nmos-test.py
+++ b/nmos-test.py
@@ -32,7 +32,7 @@ from enum import IntEnum
 
 import git
 import os
-import json
+from requests.compat import json
 import copy
 import pickle
 import random