Patchwork D339: tests: verify that peer instances only expose interface members

login
register
mail settings
Submitter phabricator
Date Aug. 11, 2017, 4:20 a.m.
Message ID <differential-rev-PHID-DREV-urwjf7e2ubovczp2rjho-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/22849/
State Superseded
Headers show

Comments

phabricator - Aug. 11, 2017, 4:20 a.m.
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Our abstract interfaces are more useful if we guarantee that
  implementations conform to certain rules. Namely, we want to ensure
  that objects implementing interfaces don't expose new public
  attributes that aren't part of the interface. That way, as long as
  consumers don't access "internal" attributes (those beginning with
  "_") then (in theory) objects implementing interfaces can be swapped
  out and everything will "just work."
  
  We add a test that enforces our "no public attributes not part
  of the abstract interface" rule.
  
  We /could/ implement "interface compliance detection" at run-time.
  However, that is littered with problems.
  
  The obvious solutions are custom __new__ and __init__ methods.
  These rely on derived types actually calling the parent's
  implementation, which is no sure bet. Furthermore, __new__ and
  __init__ will likely be called before instance-specific attributes
  are assigned. In other words, they won't detect public attributes
  set on self.__dict__. This means public attribute detection won't
  be robust.
  
  We could work around lack of robust self.__dict__ public attribute
  detection by having our interfaces implement a custom __getattribute__,
  __getattr__, and/or __setattr__. However, this incurs an undesirable
  run-time penalty. And, subclasses could override our custom
  method, bypassing the check.
  
  The most robust solution is a non-runtime test. So that's what this
  commit implements. We have a generic function for validating that an
  object only has public attributes defined by abstract classes. Then,
  we instantiate some peers and verify a newly constructed object
  plays by the rules.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D339

AFFECTED FILES
  tests/test-check-interfaces.py
  tests/test-check-interfaces.py.out

CHANGE DETAILS




To: indygreg, #hg-reviewers
Cc: mercurial-devel
phabricator - Aug. 11, 2017, 6:34 p.m.
durin42 accepted this revision.
durin42 added a comment.
This revision is now accepted and ready to land.


  Might also be neat to have a test to assert the peer and legacy peer interfaces don't overlap?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D339

To: indygreg, #hg-reviewers, durin42
Cc: durin42, mercurial-devel
phabricator - Aug. 13, 2017, 6:12 p.m.
indygreg added a comment.


  In https://phab.mercurial-scm.org/D339#5376, @durin42 wrote:
  
  > Might also be neat to have a test to assert the peer and legacy peer interfaces don't overlap?
  
  
  This can be done as a follow-up IMO.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D339

To: indygreg, #hg-reviewers, durin42
Cc: durin42, mercurial-devel

Patch

diff --git a/tests/test-check-interfaces.py.out b/tests/test-check-interfaces.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-check-interfaces.py.out
@@ -0,0 +1,2 @@ 
+public attributes not in abstract interface: badpeer.badattribute
+public attributes not in abstract interface: badpeer.badmethod
diff --git a/tests/test-check-interfaces.py b/tests/test-check-interfaces.py
new file mode 100644
--- /dev/null
+++ b/tests/test-check-interfaces.py
@@ -0,0 +1,71 @@ 
+# Test that certain objects conform to well-defined interfaces.
+
+from __future__ import absolute_import, print_function
+
+from mercurial import (
+    httppeer,
+    localrepo,
+    sshpeer,
+    ui as uimod,
+)
+
+def checkobject(o):
+    """Verify a constructed object conforms to interface rules.
+
+    An object must have __abstractmethods__ defined.
+
+    All "public" attributes of the object (attributes not prefixed with
+    an underscore) must be in __abstractmethods__ or appear on a base class
+    with __abstractmethods__.
+    """
+    name = o.__class__.__name__
+
+    allowed = set()
+    for cls in o.__class__.__mro__:
+        if not getattr(cls, '__abstractmethods__', set()):
+            continue
+
+        allowed |= cls.__abstractmethods__
+        allowed |= {a for a in dir(cls) if not a.startswith('_')}
+
+    if not allowed:
+        print('%s does not have abstract methods' % name)
+        return
+
+    public = {a for a in dir(o) if not a.startswith('_')}
+
+    for attr in sorted(public - allowed):
+        print('public attributes not in abstract interface: %s.%s' % (
+            name, attr))
+
+# Facilitates testing localpeer.
+class dummyrepo(object):
+    def __init__(self):
+        self.ui = uimod.ui()
+    def filtered(self, name):
+        pass
+    def _restrictcapabilities(self, caps):
+        pass
+
+# Facilitates testing sshpeer without requiring an SSH server.
+class testingsshpeer(sshpeer.sshpeer):
+    def _validaterepo(self, *args, **kwargs):
+        pass
+
+class badpeer(httppeer.httppeer):
+    def __init__(self):
+        super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
+        self.badattribute = True
+
+    def badmethod(self):
+        pass
+
+def main():
+    ui = uimod.ui()
+
+    checkobject(badpeer())
+    checkobject(httppeer.httppeer(ui, 'http://localhost'))
+    checkobject(localrepo.localpeer(dummyrepo()))
+    checkobject(testingsshpeer(ui, 'ssh://localhost/foo'))
+
+main()