Repository: cloudtools/troposphere

Issue:
add comparison capability to CloudFormation intrinsic function objects
When writing tests for Troposphere templates, I often check that resource properties contain the appropriate value references. Currently, only the `Ref` object has comparison capabilities (`__eq__` only) requiring me to compare attributes/method outputs instead of comparing the objects directly. This functionality should be extended to the remainder of the intrinsic functions.

From briefly looking through all of the subclasses of `AWSHelperFn` , it appears this could be implemented on the base class as all objects will have a `data` attribute that could be assessed or the `.to_dict()` method could be used to compare to raw dictionaries. 


Discussion:

Resolving patch:
diff --git a/troposphere/__init__.py b/troposphere/__init__.py
--- a/troposphere/__init__.py
+++ b/troposphere/__init__.py
@@ -351,6 +351,10 @@ def to_dict(self) -> Dict[str, Any]:
         else:
             return {}
 
+    def to_json(self, *, indent: int = 4, sort_keys: bool = True) -> str:
+        """Object as JSON."""
+        return json.dumps(self.to_dict(), indent=indent, sort_keys=sort_keys)
+
     @classmethod
     def _from_dict(
         cls: Type[__BaseAWSObjectTypeVar], title: Optional[str] = None, **kwargs: Any
@@ -411,6 +415,16 @@ def _validate_props(self) -> None:
                     msg += " (title: %s)" % title
                 raise ValueError(msg)
 
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, self.__class__):
+            return self.title == other.title and self.to_json() == other.to_json()
+        if isinstance(other, dict):
+            return {"title": self.title, **self.to_dict()} == other
+        return False
+
+    def __ne__(self, other: object) -> bool:
+        return not self == other
+
 
 class AWSObject(BaseAWSObject):
     dictname = "Properties"
@@ -491,6 +505,23 @@ def getdata(self, data: object) -> Any:
     def to_dict(self) -> Any:
         return encode_to_dict(self.data)
 
+    def to_json(self, *, indent: int = 4, sort_keys: bool = True) -> str:
+        """Object as JSON."""
+        return json.dumps(self.to_dict(), indent=indent, sort_keys=sort_keys)
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, self.__class__):
+            return self.to_json() == other.to_json()
+        if isinstance(other, (dict, list)):
+            return self.to_dict() == other
+        return False
+
+    def __hash__(self) -> int:
+        return hash(self.to_json(indent=0))
+
+    def __ne__(self, other: object) -> bool:
+        return not self == other
+
 
 class GenericHelperFn(AWSHelperFn):
     """Used as a fallback for the template generator"""
@@ -498,9 +529,6 @@ class GenericHelperFn(AWSHelperFn):
     def __init__(self, data: Any):
         self.data = self.getdata(data)
 
-    def to_dict(self) -> Any:
-        return encode_to_dict(self.data)
-
 
 class Base64(AWSHelperFn):
     def __init__(self, data: Any) -> None:


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -5,6 +5,7 @@
     AWSObject,
     AWSProperty,
     Cidr,
+    GenericHelperFn,
     If,
     Join,
     NoValue,
@@ -24,7 +25,74 @@
 from troposphere.validators import positive_integer
 
 
+def double(x):
+    return positive_integer(x) * 2
+
+
+def call_correct(x):
+    return x
+
+
+def call_incorrect(x):
+    raise ValueError
+
+
+class FakeAWSProperty(AWSProperty):
+    props = {}
+
+
+class FakeAWSObject(AWSObject):
+    type = "Fake::AWS::Object"
+
+    props = {
+        "callcorrect": (call_correct, False),
+        "callincorrect": (call_incorrect, False),
+        "singlelist": (list, False),
+        "multilist": ([bool, int, float], False),
+        "multituple": ((bool, int), False),
+        "helperfun": (positive_integer, False),
+        "listhelperfun": ([double], False),
+    }
+
+    def validate(self):
+        properties = self.properties
+        title = self.title
+        type = self.type
+        if "callcorrect" in properties and "singlelist" in properties:
+            raise ValueError(
+                (
+                    "Cannot specify both 'callcorrect and 'singlelist' in "
+                    "object %s (type %s)" % (title, type)
+                )
+            )
+
+
 class TestBasic(unittest.TestCase):
+    def test___eq__(self):
+        """Test __eq__."""
+        assert FakeAWSObject("foobar", callcorrect=True) == FakeAWSObject(
+            "foobar", callcorrect=True
+        )
+        assert FakeAWSObject("foobar", callcorrect=True) == {
+            "title": "foobar",
+            "Properties": {"callcorrect": True},
+        }
+        assert GenericHelperFn("foobar") == GenericHelperFn("foobar")
+        assert GenericHelperFn({"foo": "bar"}) == {"foo": "bar"}
+
+    def test___ne__(self):
+        """Test __ne__."""
+        assert FakeAWSObject("foo", callcorrect=True) != FakeAWSObject(
+            "bar", callcorrect=True
+        )
+        assert FakeAWSObject("foobar", callcorrect=True) != FakeAWSObject(
+            "foobar", callcorrect=False
+        )
+        assert FakeAWSObject("foobar", callcorrect=True) != FakeAWSProperty("foobar")
+        assert GenericHelperFn("foobar") != GenericHelperFn("bar")
+        assert GenericHelperFn("foobar") != "foobar"
+        assert GenericHelperFn("foobar") != FakeAWSProperty("foobar")
+
     def test_badproperty(self):
         with self.assertRaises(AttributeError):
             Instance(
@@ -89,48 +157,6 @@ def test_pickle_ok(self):
         self.assertEqual(b2.BucketName, b.BucketName)
 
 
-def double(x):
-    return positive_integer(x) * 2
-
-
-def call_correct(x):
-    return x
-
-
-def call_incorrect(x):
-    raise ValueError
-
-
-class FakeAWSObject(AWSObject):
-    type = "Fake::AWS::Object"
-
-    props = {
-        "callcorrect": (call_correct, False),
-        "callincorrect": (call_incorrect, False),
-        "singlelist": (list, False),
-        "multilist": ([bool, int, float], False),
-        "multituple": ((bool, int), False),
-        "helperfun": (positive_integer, False),
-        "listhelperfun": ([double], False),
-    }
-
-    def validate(self):
-        properties = self.properties
-        title = self.title
-        type = self.type
-        if "callcorrect" in properties and "singlelist" in properties:
-            raise ValueError(
-                (
-                    "Cannot specify both 'callcorrect and 'singlelist' in "
-                    "object %s (type %s)" % (title, type)
-                )
-            )
-
-
-class FakeAWSProperty(AWSProperty):
-    props = {}
-
-
 class TestValidators(unittest.TestCase):
     def test_callcorrect(self):
         FakeAWSObject("fake", callcorrect=True)