Compare commits

...

1 Commits

Author SHA1 Message Date
Joona Hoikkala
49ae6d4921 Dual parsernode implementation 2019-09-10 00:11:07 +03:00
4 changed files with 750 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
from certbot_apache import interfaces
PASS = "CERTBOT_PASS_ASSERT"
def assertEqual(first, second):
""" Equality assertion """
if isinstance(first, interfaces.CommentNode):
assertEqualComment(first, second)
elif isinstance(first, interfaces.DirectiveNode):
assertEqualDirective(first, second)
elif isinstance(first, interfaces.BlockNode):
assertEqualBlock(first, second)
# Skip tests if filepath includes the pass value. This is done
# because filepath is variable of the base ParserNode interface, and
# unless the implementation is actually done, we cannot assume getting
# correct results from boolean assertion for dirty
if not isPass(first.filepath, second.filepath):
assert first.dirty == second.dirty
# We might want to disable this later if testing with two separate
# (but identical) directory structures.
assert first.filepath == second.filepath
def assertEqualComment(first, second):
""" Equality assertion for CommentNode """
assert isinstance(first, interfaces.CommentNode)
assert isinstance(second, interfaces.CommentNode)
if not isPass(first.comment, second.comment):
assert first.comment == second.comment
if not isPass(first.filepath, second.filepath):
assert first.filepath == second.filepath
def _assertEqualDirectiveComponents(first, second):
""" Handles assertion for instance variables for DirectiveNode and BlockNode"""
# Enabled value cannot be asserted, because Augeas implementation
# is unable to figure that out.
# assert first.enabled == second.enabled
if not isPass(first.name, second.name):
assert first.name == second.name
if not isPass(first.filepath, second.filepath):
assert first.filepath == second.filepath
if not isPass(first.parameters, second.parameters):
assert first.parameters == second.parameters
def assertEqualDirective(first, second):
""" Equality assertion for DirectiveNode """
assert isinstance(first, interfaces.DirectiveNode)
assert isinstance(second, interfaces.DirectiveNode)
_assertEqualDirectiveComponents(first, second)
def assertEqualBlock(first, second):
""" Equality assertion for BlockNode """
# first was checked in the assertEqual method
assert isinstance(first, interfaces.BlockNode)
assert isinstance(second, interfaces.BlockNode)
_assertEqualDirectiveComponents(first, second)
# Children cannot be asserted, because Augeas implementation will not
# prepopulate the sequence of children.
# assert len(first.children) == len(second.children)
def isPass(first, second):
""" Checks if either first or second holds the assertion pass value """
if isinstance(first, tuple) or isinstance(first, list):
if PASS in first:
return True
if isinstance(second, tuple) or isinstance(second, list):
if PASS in second:
return True
if PASS in [first, second]:
return True
return False
def isPassNodeList(nodelist):
""" Checks if a ParserNode in the nodelist should pass the assertion,
this function is used for results of find_* methods. Unimplemented find_*
methods should return a sequence containing a single ParserNode instance
with assertion pass string."""
try:
node = nodelist[0]
except IndexError:
node = None
if not node:
# Empty result means that the method is implemented
return False
if isinstance(node, interfaces.BlockNode):
return _isPassDirective(node)
if isinstance(node, interfaces.DirectiveNode):
return _isPassDirective(node)
return _isPassComment(node)
def _isPassDirective(block):
""" Checks if BlockNode or DirectiveNode should pass the assertion """
if block.name == PASS:
return True
if PASS in block.parameters:
return True
if block.filepath == PASS:
return True
return False
def _isPassComment(comment):
""" Checks if CommentNode should pass the assertion """
if comment.comment == PASS:
return True
if comment.filepath == PASS:
return True
return False
def assertSimple(first, second):
""" Simple assertion """
if not isPass(first, second):
assert first == second
def assertSimpleList(first, second):
""" Simple assertion that lists contain the same objects. This needs to
be used when there's uncertainty about the ordering of the list. """
if not isPass(first, second):
if first:
for f in first:
assert f in second

View File

@@ -0,0 +1,127 @@
""" Augeas implementation of the ParserNode interface """
from certbot_apache import assertions
from certbot_apache import interfaces
from certbot_apache import parsernode_util as util
class AugeasParserNode(interfaces.ParserNode):
""" Augeas implementation of ParserNode interface """
def __init__(self, **kwargs):
ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs)
self.ancestor = ancestor
# self.filepath = filepath
self.filepath = assertions.PASS
self.dirty = dirty
self.metadata = metadata
def save(self, msg): # pragma: no cover
pass
class AugeasCommentNode(AugeasParserNode):
""" Augeas implementation of CommentNode interface """
def __init__(self, **kwargs):
comment, kwargs = util.commentnode_kwargs(kwargs)
super(AugeasCommentNode, self).__init__(**kwargs)
self.comment = comment
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.comment == other.comment and
self.filepath == other.filepath and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
class AugeasDirectiveNode(AugeasParserNode):
""" Augeas implementation of DirectiveNode interface """
def __init__(self, **kwargs):
name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
super(AugeasDirectiveNode, self).__init__(**kwargs)
self.name = name
self.parameters = parameters
self.enabled = enabled
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
def set_parameters(self, parameters):
self.parameters = parameters
class AugeasBlockNode(AugeasDirectiveNode):
""" Augeas implementation of BlockNode interface """
def __init__(self, **kwargs):
super(AugeasBlockNode, self).__init__(**kwargs)
self.children = ()
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.children == other.children and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
def add_child_block(self, name, parameters=None, position=None):
new_block = AugeasBlockNode(name=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)
self.children += (new_block,)
return new_block
def add_child_directive(self, name, parameters=None, position=None):
new_dir = AugeasDirectiveNode(name=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)
self.children += (new_dir,)
return new_dir
def add_child_comment(self, comment="", position=None):
new_comment = AugeasCommentNode(comment=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)
self.children += (new_comment,)
return new_comment
def find_blocks(self, name, exclude=True):
return [AugeasBlockNode(name=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)]
def find_directives(self, name, exclude=True):
return [AugeasDirectiveNode(name=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)]
def find_comments(self, comment, exact=False):
return [AugeasCommentNode(comment=assertions.PASS,
ancestor=self,
filepath=assertions.PASS)]
def delete_child(self, child): # pragma: no cover
pass
def unsaved_files(self): # pragma: no cover
return [assertions.PASS]
interfaces.CommentNode.register(AugeasCommentNode)
interfaces.DirectiveNode.register(AugeasDirectiveNode)
interfaces.BlockNode.register(AugeasBlockNode)

View File

@@ -0,0 +1,318 @@
""" Tests for ParserNode interface """
from certbot_apache import assertions
from certbot_apache import augeasparser
class DualNodeBase(object):
""" Dual parser interface for in development testing. This is used as the
base class for dual parser interface classes. This class handles runtime
attribute value assertions."""
def save(self, msg): # pragma: no cover
""" Call save for both parsers """
self.primary.save(msg)
self.secondary.save(msg)
def __getattr__(self, aname):
""" Attribute value assertion """
firstval = getattr(self.primary, aname)
secondval = getattr(self.secondary, aname)
if not assertions.isPass(firstval, secondval):
assertions.assertSimple(firstval, secondval)
return firstval
class DualCommentNode(DualNodeBase):
""" Dual parser implementation of CommentNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of CommentNode objects as well as creating a DualCommentNode object
using precreated or fetched CommentNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.CommentNode:
:param CommentNode primary: Primary pre-created CommentNode, mainly
used when creating new DualParser nodes using add_* methods.
:param CommentNode secondary: Secondary pre-created CommentNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if not primary:
self.primary = augeasparser.AugeasCommentNode(**kwargs)
else:
self.primary = primary
if not secondary:
self.secondary = augeasparser.AugeasCommentNode(**kwargs)
else:
self.secondary = secondary
assertions.assertEqual(self.primary, self.secondary)
class DualDirectiveNode(DualNodeBase):
""" Dual parser implementation of DirectiveNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of DirectiveNode objects as well as creating a DualDirectiveNode object
using precreated or fetched DirectiveNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.DirectiveNode:
:param DirectiveNode primary: Primary pre-created DirectiveNode, mainly
used when creating new DualParser nodes using add_* methods.
:param DirectiveNode secondary: Secondary pre-created DirectiveNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if not primary:
self.primary = augeasparser.AugeasDirectiveNode(**kwargs)
else:
self.primary = primary
if not secondary:
self.secondary = augeasparser.AugeasDirectiveNode(**kwargs)
else:
self.secondary = secondary
assertions.assertEqual(self.primary, self.secondary)
def set_parameters(self, parameters):
""" Sets parameters and asserts that both implementation successfully
set the parameter sequence """
self.primary.set_parameters(parameters)
self.secondary.set_parameters(parameters)
assertions.assertEqual(self.primary, self.secondary)
class DualBlockNode(DualDirectiveNode):
""" Dual parser implementation of BlockNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of BlockNode objects as well as creating a DualBlockNode object
using precreated or fetched BlockNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.BlockNode:
:param BlockNode primary: Primary pre-created BlockNode, mainly
used when creating new DualParser nodes using add_* methods.
:param BlockNode secondary: Secondary pre-created BlockNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if not primary:
self.primary = augeasparser.AugeasBlockNode(**kwargs)
else:
self.primary = primary
if not secondary:
self.secondary = augeasparser.AugeasBlockNode(**kwargs)
else:
self.secondary = secondary
assertions.assertEqual(self.primary, self.secondary)
def add_child_block(self, name, parameters=None, position=None):
""" Creates a new child BlockNode, asserts that both implementations
did it in a similar way, and returns a newly created DualBlockNode object
encapsulating both of the newly created objects """
primary_new = self.primary.add_child_block(name, parameters, position)
secondary_new = self.secondary.add_child_block(name, parameters, position)
assertions.assertEqual(primary_new, secondary_new)
new_block = DualBlockNode(primary=primary_new, secondary=secondary_new)
return new_block
def add_child_directive(self, name, parameters=None, position=None):
""" Creates a new child DirectiveNode, asserts that both implementations
did it in a similar way, and returns a newly created DualDirectiveNode
object encapsulating both of the newly created objects """
primary_new = self.primary.add_child_directive(name, parameters, position)
secondary_new = self.secondary.add_child_directive(name, parameters, position)
assertions.assertEqual(primary_new, secondary_new)
new_dir = DualDirectiveNode(primary=primary_new, secondary=secondary_new)
return new_dir
def add_child_comment(self, comment="", position=None):
""" Creates a new child CommentNode, asserts that both implementations
did it in a similar way, and returns a newly created DualCommentNode
object encapsulating both of the newly created objects """
primary_new = self.primary.add_child_comment(comment, position)
secondary_new = self.secondary.add_child_comment(comment, position)
assertions.assertEqual(primary_new, secondary_new)
new_comment = DualCommentNode(primary=primary_new, secondary=secondary_new)
return new_comment
def _create_matching_list(self, primary_list, secondary_list): # pragma: no cover
""" Matches the list of primary_list to a list of secondary_list and
returns a list of tuples. This is used to create results for find_
methods. """
matched = list()
for p in primary_list:
match = None
for s in secondary_list:
try:
assertions.assertEqual(p, s)
match = s
except AssertionError:
continue
if match:
matched.append((p,s))
else:
raise AssertionError("Could not find a matching node.")
return matched
def find_blocks(self, name, exclude=True): # pragma: no cover
"""
Performs a search for BlockNodes using both implementations and does simple
checks for results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualBlockNode
instances that encapsulate the pairs of returned BlockNode objects.
"""
primary_blocks = self.primary.find_blocks(name, exclude)
secondary_blocks = self.secondary.find_blocks(name, exclude)
# The order of search results for Augeas implementation cannot be
# assured.
pass_primary = assertions.isPassNodeList(primary_blocks)
pass_secondary = assertions.isPassNodeList(secondary_blocks)
new_blocks = list()
if pass_primary and pass_secondary:
# Both unimplemented
new_blocks.append(DualBlockNode(primary=primary_blocks[0],
secondary=secondary_blocks[0]))
elif pass_primary:
for c in secondary_blocks:
new_blocks.append(DualBlockNode(primary=primary_blocks[0],
secondary=c))
elif pass_secondary:
for c in primary_blocks:
new_blocks.append(DualBlockNode(primary=c,
secondary=secondary_blocks[0]))
else:
assert len(primary_blocks) == len(secondary_blocks)
matches = self._create_matching_list(primary_blocks, secondary_blocks)
for p,s in matches:
new_blocks.append(DualBlockNode(primary=p, secondary=s))
return new_blocks
def find_directives(self, name, exclude=True): # pragma: no cover
"""
Performs a search for DirectiveNodes using both implementations and
checks the results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualDirectiveNode
instances that encapsulate the pairs of returned DirectiveNode objects.
"""
primary_dirs = self.primary.find_directives(name, exclude)
secondary_dirs = self.secondary.find_directives(name, exclude)
# The order of search results for Augeas implementation cannot be
# assured.
pass_primary = assertions.isPassNodeList(primary_dirs)
pass_secondary = assertions.isPassNodeList(secondary_dirs)
new_dirs = list()
if pass_primary and pass_secondary:
# Both unimplemented
new_dirs.append(DualDirectiveNode(primary=primary_dirs[0],
secondary=secondary_dirs[0]))
elif pass_primary:
for c in secondary_dirs:
new_dirs.append(DualDirectiveNode(primary=primary_dirs[0],
secondary=c))
elif pass_secondary:
for c in primary_dirs:
new_dirs.append(DualDirectiveNode(primary=c,
secondary=secondary_dirs[0]))
else:
assert len(primary_dirs) == len(secondary_dirs)
matches = self._create_matching_list(primary_dirs, secondary_dirs)
for p,s in matches:
new_dirs.append(DualDirectiveNode(primary=p, secondary=s))
return new_dirs
def find_comments(self, comment, exact=False): # pragma: no cover
"""
Performs a search for CommentNodes using both implementations and
checks the results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualCommentNode
instances that encapsulate the pairs of returned CommentNode objects.
"""
primary_com = self.primary.find_comments(comment, exact)
secondary_com = self.secondary.find_comments(comment, exact)
# The order of search results for Augeas implementation cannot be
# assured.
pass_primary = assertions.isPassNodeList(primary_com)
pass_secondary = assertions.isPassNodeList(secondary_com)
new_com = list()
if pass_primary and pass_secondary:
# Both unimplemented
new_com.append(DualCommentNode(primary=primary_com[0],
secondary=secondary_com[0]))
elif pass_primary:
for c in secondary_com:
new_com.append(DualCommentNode(primary=primary_com[0],
secondary=c))
elif pass_secondary:
for c in primary_com:
new_com.append(DualCommentNode(primary=c,
secondary=secondary_com[0]))
else:
assert len(primary_com) == len(secondary_com)
matches = self._create_matching_list(primary_com, secondary_com)
for p,s in matches:
new_com.append(DualCommentNode(primary=p, secondary=s))
return new_com
def delete_child(self, child): # pragma: no cover
"""Deletes a child from the ParserNode implementations. The actual
ParserNode implementations are used here directly in order to be able
to match a child to the list of children."""
self.primary.delete_child(child.primary)
self.secondary.delete_child(child.secondary)
def unsaved_files(self):
""" Fetches the list of unsaved file paths and asserts that the lists
match """
primary_files = self.primary.unsaved_files()
secondary_files = self.secondary.unsaved_files()
assertions.assertSimple(primary_files, secondary_files)
return primary_files

View File

@@ -0,0 +1,167 @@
"""Tests for DualParserNode implementation"""
import unittest
import mock
from certbot_apache import assertions
from certbot_apache import dualparser
class DualParserNodeTest(unittest.TestCase):
"""DualParserNode tests"""
def setUp(self): # pylint: disable=arguments-differ
self.block = dualparser.DualBlockNode(name="block",
ancestor=None,
filepath="/tmp/something")
self.block_two = dualparser.DualBlockNode(name="block",
ancestor=self.block,
filepath="/tmp/something")
self.directive = dualparser.DualDirectiveNode(name="directive",
ancestor=self.block,
filepath="/tmp/something")
self.comment = dualparser.DualCommentNode(comment="comment",
ancestor=self.block,
filepath="/tmp/something")
def test_create_with_primary(self):
cnode = dualparser.DualCommentNode(comment="comment",
ancestor=self.block,
filepath="/tmp/something",
primary=self.comment.secondary)
dnode = dualparser.DualDirectiveNode(name="directive",
ancestor=self.block,
filepath="/tmp/something",
primary=self.directive.secondary)
bnode = dualparser.DualBlockNode(name="block",
ancestor=self.block,
filepath="/tmp/something",
primary=self.block.secondary)
self.assertTrue(cnode.primary is self.comment.secondary)
self.assertTrue(dnode.primary is self.directive.secondary)
self.assertTrue(bnode.primary is self.block.secondary)
def test_create_with_secondary(self):
cnode = dualparser.DualCommentNode(comment="comment",
ancestor=self.block,
filepath="/tmp/something",
secondary=self.comment.primary)
dnode = dualparser.DualDirectiveNode(name="directive",
ancestor=self.block,
filepath="/tmp/something",
secondary=self.directive.primary)
bnode = dualparser.DualBlockNode(name="block",
ancestor=self.block,
filepath="/tmp/something",
secondary=self.block.primary)
self.assertTrue(cnode.secondary is self.comment.primary)
self.assertTrue(dnode.secondary is self.directive.primary)
self.assertTrue(bnode.secondary is self.block.primary)
def test_set_params(self):
params = ("first", "second")
self.directive.set_parameters(params)
self.assertEqual(self.directive.primary.parameters, params)
self.assertEqual(self.directive.secondary.parameters, params)
def test_set_parameters(self):
pparams = mock.MagicMock()
sparams = mock.MagicMock()
pparams.parameters = ("a", "b")
sparams.parameters = ("a", "b")
self.directive.primary.set_parameters = pparams
self.directive.secondary.set_parameters = sparams
self.directive.set_parameters(("param","seq"))
self.assertTrue(pparams.called)
self.assertTrue(sparams.called)
def test_delete_child(self):
pdel = mock.MagicMock()
sdel = mock.MagicMock()
self.block.primary.delete_child = pdel
self.block.secondary.delete_child = sdel
self.block.delete_child(self.comment)
self.assertTrue(pdel.called)
self.assertTrue(sdel.called)
def test_unsaved_files(self):
puns = mock.MagicMock()
suns = mock.MagicMock()
puns.return_value = assertions.PASS
suns.return_value = assertions.PASS
self.block.primary.unsaved_files = puns
self.block.secondary.unsaved_files = suns
self.block.unsaved_files()
self.assertTrue(puns.called)
self.assertTrue(suns.called)
def test_add_child_block(self):
p = mock.MagicMock()
p.return_value = self.block
s = mock.MagicMock()
s.return_value = self.block
self.block.primary.add_child_block = p
self.block.secondary.add_child_block = s
self.block.add_child_block("name")
self.assertTrue(p.called)
self.assertTrue(s.called)
def test_add_child_directive(self):
p = mock.MagicMock()
p.return_value = self.directive
s = mock.MagicMock()
s.return_value = self.directive
self.block.primary.add_child_directive = p
self.block.secondary.add_child_directive = s
self.block.add_child_directive("name")
self.assertTrue(p.called)
self.assertTrue(s.called)
def test_add_child_comment(self):
p = mock.MagicMock()
p.return_value = self.comment
s = mock.MagicMock()
s.return_value = self.comment
self.block.primary.add_child_comment = p
self.block.secondary.add_child_comment = s
self.block.add_child_comment("comment")
self.assertTrue(p.called)
self.assertTrue(s.called)
def test_find_blocks(self):
dblks = self.block.find_blocks("block")
p_dblks = [d.primary for d in dblks]
s_dblks = [d.secondary for d in dblks]
p_blks = self.block.primary.find_blocks("block")
s_blks = self.block.secondary.find_blocks("block")
# Check that every block response is represented in the list of
# DualParserNode instances.
for p in p_dblks:
self.assertTrue(p in p_blks)
for s in s_dblks:
self.assertTrue(s in s_blks)
def test_find_directives(self):
ddirs = self.block.find_directives("directive")
p_ddirs = [d.primary for d in ddirs]
s_ddirs = [d.secondary for d in ddirs]
p_dirs = self.block.primary.find_directives("directive")
s_dirs = self.block.secondary.find_directives("directive")
# Check that every directive response is represented in the list of
# DualParserNode instances.
for p in p_ddirs:
self.assertTrue(p in p_dirs)
for s in s_ddirs:
self.assertTrue(s in s_dirs)
def test_find_comments(self):
dcoms = self.block.find_comments("comment")
p_dcoms = [d.primary for d in dcoms]
s_dcoms = [d.secondary for d in dcoms]
p_coms = self.block.primary.find_comments("comment")
s_coms = self.block.secondary.find_comments("comment")
# Check that every comment response is represented in the list of
# DualParserNode instances.
for p in p_dcoms:
self.assertTrue(p in p_coms)
for s in s_dcoms:
self.assertTrue(s in s_coms)