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

2017-08-10 Thread indygreg (Gregory Szorc)
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

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()



To: indygreg, #hg-reviewers
Cc: mercurial-devel
___
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel


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

2017-08-11 Thread durin42 (Augie Fackler)
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
___
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel


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

2017-08-13 Thread indygreg (Gregory Szorc)
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
___
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel


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

2017-08-15 Thread indygreg (Gregory Szorc)
This revision was automatically updated to reflect the committed changes.
Closed by commit rHGb70029f355a3: tests: verify that peer instances only expose 
interface members (authored by indygreg).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D339?vs=764&id=938

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

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

CHANGE DETAILS

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()



To: indygreg, #hg-reviewers, durin42
Cc: durin42, mercurial-devel
___
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel