Compare commits

...

19 Commits

Author SHA1 Message Date
Joona Hoikkala
ff0d851a1e WIP 2019-08-05 21:01:18 +03:00
Joona Hoikkala
5cfdfc80bd Basic assertions 2019-08-05 15:57:53 +03:00
Joona Hoikkala
f4c962073e Merge remote-tracking branch 'origin/apache-parser-v2' into parser_refactor 2019-08-05 15:34:12 +03:00
Joona Hoikkala
973c521c3e Merge remote-tracking branch 'origin/master' into parser_refactor 2019-08-02 18:53:39 +03:00
Joona Hoikkala
526997107b Merge branch 'parser_refactor1' into parser_refactor 2019-08-02 18:53:08 +03:00
Joona Hoikkala
c1362b7437 Add more context to comment property docstring 2019-08-02 18:11:37 +03:00
Joona Hoikkala
371ece9ea6 Fix docstrings and mypy 2019-08-02 15:55:46 +03:00
Joona Hoikkala
dd762f3c79 Make coverage happy 2019-08-01 17:05:13 +03:00
Joona Hoikkala
8cfd228eee Add parameters, fix mypy and lint issues 2019-08-01 16:18:43 +03:00
Joona Hoikkala
0aef744f29 Add dummy tests and change arguments to properties 2019-07-29 22:01:13 +03:00
Joona Hoikkala
4b77350b0a Update certbot-apache/certbot_apache/interfaces.py
Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-07-29 18:52:14 +03:00
Joona Hoikkala
6b5c4c9f79 Update certbot-apache/certbot_apache/interfaces.py
Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-07-29 18:52:04 +03:00
Joona Hoikkala
267a281a10 Make linter happy 2019-07-25 19:34:10 +03:00
Joona Hoikkala
fbd7d20d47 Move parserassert to a different PR and address some of the comments 2019-07-24 17:22:39 +03:00
Joona Hoikkala
41f24c73f5 Move include handling to its own method 2019-07-24 16:35:19 +03:00
Joona Hoikkala
f74b8ad2ad Update certbot-apache/certbot_apache/interfaces.py
Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-07-23 13:20:16 +03:00
Joona Hoikkala
d8806a4c05 Add the test assertions, and fix interface 2019-07-22 16:18:08 +03:00
Joona Hoikkala
fd3644a4d7 Address some of the review comments 2019-07-19 18:48:50 +03:00
Joona Hoikkala
0b545ab443 New ParserNode interface abstraction 2019-07-15 18:33:19 +03:00
7 changed files with 740 additions and 16 deletions

View File

@@ -0,0 +1,87 @@
""" Tests for ParserNode interface """
from certbot_apache import interfaces
class AugeasCommentNode(interfaces.CommentNode):
""" Augeas implementation of CommentNode interface """
ancestor = None
comment = ""
dirty = False
def __init__(self, comment, ancestor=None):
self.comment = comment
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
class AugeasDirectiveNode(interfaces.DirectiveNode):
""" Augeas implementation of DirectiveNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
dirty = False
enabled = True
name = ""
def __init__(self, name, parameters=tuple(), ancestor=None):
self.name = name
self.parameters = parameters
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
self.parameters = tuple("CERTBOT_PASS_ASSERT")
class AugeasBlockNode(interfaces.BlockNode):
""" Augeas implementation of BlockNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
children = tuple() # type: Tuple[interfaces.ParserNode, ...]
dirty = False
enabled = True
name = ""
def __init__(self, name, parameters=tuple(), ancestor=None):
self.name = name
self.parameters = parameters
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
new_block = AugeasBlockNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_block,)
return new_block
def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
new_dir = AugeasDirectiveNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_dir,)
return new_dir
def add_child_comment(self, comment="", position=None): # pragma: no cover
new_comment = AugeasCommentNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_comment,)
return new_comment
def find_blocks(self, name, exclude=True): # pragma: no cover
return [AugeasBlockNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def find_directives(self, name, exclude=True): # pragma: no cover
return [AugeasDirectiveNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def find_comments(self, comment, exact=False): # pragma: no cover
return [AugeasCommentNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def delete_child(self, child): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
self.parameters = tuple("CERTBOT_PASS_ASSERT")
def unsaved_files(self): # pragma: no cover
return ["CERTBOT_PASS_ASSERT"]

View File

@@ -29,6 +29,7 @@ from certbot.plugins.util import path_surgery
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot_apache import apache_util
from certbot_apache import augeasparser
from certbot_apache import constants
from certbot_apache import display_ops
from certbot_apache import http_01
@@ -280,6 +281,10 @@ class ApacheConfigurator(common.Installer):
"Unable to create a lock file in {0}. Are you running"
" Certbot with sufficient privileges to modify your"
" Apache configuration?".format(self.option("server_root")))
# TODO: apache-parser-v2
self.parser_root = augeasparser.AugeasBlockNode(None, ancestor=None)
self._prepared = True
def save(self, title=None, temporary=False):
@@ -507,10 +512,25 @@ class ApacheConfigurator(common.Installer):
"cert_key": self.parser.find_dir("SSLCertificateKeyFile",
None, vhost.path)}
# TODO: apache-parser-v2
# We need to add a BlockNode reference to obj.VirtualHost
# v2_path = {"cert_path": vhost.node.find_directives(
# "SSLCertificateFile"),
# "cert_key": vhost.node.find_directives(
# "SSLCertificateKeyFile")}
# parserassert.legacy_assert_dir(path["cert_path"], v2_path["cert_path"])
# parserassert.legacy_assert_dir(path["cert_key"], v2_path["cert_key"])
# Only include if a certificate chain is specified
if chain_path is not None:
path["chain_path"] = self.parser.find_dir(
"SSLCertificateChainFile", None, vhost.path)
# TODO: apache-parser-v2
# v2_path["chain_path"] = vhost.node.find_directives(
# "SSLCertificateChainFile")
# parserassert.legacy_assert_dir(path["chain_path"],
# v2_path["chain_path])
# Handle errors when certificate/key directives cannot be found
if not path["cert_path"]:
@@ -535,9 +555,15 @@ class ApacheConfigurator(common.Installer):
set_cert_path = cert_path
self.parser.aug.set(path["cert_path"][-1], cert_path)
self.parser.aug.set(path["cert_key"][-1], key_path)
# TODO: apache-parser-v2
# path["cert_path"][-1].set_parameters(cert_path,)
# path["cert_key"][-1].set_parameters(key_path,)
if chain_path is not None:
self.parser.add_dir(vhost.path,
"SSLCertificateChainFile", chain_path)
# TODO: apache-parser-v2
# vhost.node.add_child_directive("SSLCertificateChainFile",
# (chain_path,))
else:
raise errors.PluginError("--chain-path is required for your "
"version of Apache")
@@ -548,6 +574,9 @@ class ApacheConfigurator(common.Installer):
set_cert_path = fullchain_path
self.parser.aug.set(path["cert_path"][-1], fullchain_path)
self.parser.aug.set(path["cert_key"][-1], key_path)
# TODO: apache-parser-v2
# path["cert_path"][-1].set_parameters(fullchain_path,)
# path["cert_key"][-1].set_parameters(key_path,)
# Enable the new vhost if needed
if not vhost.enabled:
@@ -793,29 +822,48 @@ class ApacheConfigurator(common.Installer):
return ""
def _get_vhost_names(self, path):
def _get_vhost_names(self, vhost):
"""Helper method for getting the ServerName and
ServerAlias values from vhost in path
ServerAlias values from vhost
:param path: Path to read ServerName and ServerAliases from
:param vhost: VirtualHost object to read ServerName and ServerAliases from
:returns: Tuple including ServerName and `list` of ServerAlias strings
"""
servername_match = self.parser.find_dir(
"ServerName", None, start=path, exclude=False)
"ServerName", None, start=vhost.path, exclude=False)
serveralias_match = self.parser.find_dir(
"ServerAlias", None, start=path, exclude=False)
"ServerAlias", None, start=vhost.path, exclude=False)
# TODO: apache-parser-v2
# v2_servername_match = vhost.node.find_directives("ServerName",
# exclude=False)
# v2_serveralias_match = vhost.node.find_directives("ServerAlias",
# exclude=False)
# parserassert.legacy_assert_dir(servername_match, v2_servername_match)
# parserassert.legacy_assert_dir(serveralias_match, v2_serveralias_match)
serveraliases = []
for alias in serveralias_match:
serveralias = self.parser.get_arg(alias)
serveraliases.append(serveralias)
# TODO: apache-parser-v2
# v2_serveraliases = []
# for alias in v2_serveralias_match:
# v2_serveraliases += list(alias.parameters)
# parserassert.legacy_assert_list(serveraliases, v2_serveraliases)
servername = None
if servername_match:
# Get last ServerName as each overwrites the previous
servername = self.parser.get_arg(servername_match[-1])
# TODO: apache-parser-v2
# v2_servername = None
# if v2_servername_match:
# v2_servername = v2_servername_match[-1].parameters[-1]
# parserassert.simpleassert(servername, v2_servername)
return (servername, serveraliases)
@@ -827,7 +875,7 @@ class ApacheConfigurator(common.Installer):
"""
servername, serveraliases = self._get_vhost_names(host.path)
servername, serveraliases = self._get_vhost_names(host)
for alias in serveraliases:
if not host.modmacro:
@@ -1476,7 +1524,7 @@ class ApacheConfigurator(common.Installer):
def _add_servername_alias(self, target_name, vhost):
vh_path = vhost.path
sname, saliases = self._get_vhost_names(vh_path)
sname, saliases = self._get_vhost_names(vhost)
if target_name == sname or target_name in saliases:
return
if self._has_matching_wildcard(vh_path, target_name):

View File

@@ -0,0 +1,375 @@
"""Parser interfaces."""
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class ParserNode(object):
"""
ParserNode is the basic building block of the tree of such nodes,
representing the structure of the configuration. It is largely meant to keep
the structure information intact and idiomatically accessible.
The root node as well as the child nodes of it should be instances of ParserNode.
Nodes keep track of their differences to on-disk representation of configuration
by marking modified ParserNodes as dirty to enable partial write-to-disk for
different files in the configuration structure.
While for the most parts the usage and the child types are obvious, "include"-
and similar directives are an exception to this rule. This is because of the
nature of include directives - which unroll the contents of another file or
configuration block to their place. While we could unroll the included nodes
to the parent tree, it remains important to keep the context of include nodes
separate in order to write back the original configuration as it was.
For parsers that require the implementation to keep track of the whitespacing,
it's responsibility of each ParserNode object itself to store its prepending
whitespaces in order to be able to reconstruct the complete configuration file i
as it was when originally read from the disk.
"""
@property
@abc.abstractmethod
def ancestor(self): # pragma: no cover
"""
This property contains a reference to ancestor node, or None if the node
is the root node of the configuration tree.
:returns: The ancestor BlockNode object, or None for root node.
:rtype: ParserNode
"""
raise NotImplementedError
@property
@abc.abstractmethod
def dirty(self): # pragma: no cover
"""
This property contains a boolean value of the information if this node has
been modified since last save (or after the initial parse).
:returns: True if this node has had changes that have not yet been written
to disk.
:rtype: bool
"""
raise NotImplementedError
@abc.abstractmethod
def save(self, msg):
"""
Save traverses the children, and attempts to write the AST to disk for
all the objects that are marked dirty. The actual operation of course
depends on the underlying implementation. save() shouldn't be called
from the Configurator outside of its designated save() method in order
to ensure that the Reverter checkpoints are created properly.
Note: this approach of keeping internal structure of the configuration
within the ParserNode tree does not represent the file inclusion structure
of actual configuration files that reside in the filesystem. To handle
file writes properly, the file specific temporary trees should be extracted
from the full ParserNode tree where necessary when writing to disk.
"""
@six.add_metaclass(abc.ABCMeta)
class CommentNode(ParserNode):
"""
CommentNode class is used for representation of comments within the parsed
configuration structure. Because of the nature of comments, it is not able
to have child nodes and hence it is always treated as a leaf node.
CommentNode stores its contents in class variable 'comment' and does not
have a specific name.
"""
@property
@abc.abstractmethod
def comment(self): # pragma: no cover
"""
Comment property contains the contents of the comment without the comment
directives (typically # or /* ... */).
:returns: A string containing the comment
:rtype: str
"""
raise NotImplementedError
@six.add_metaclass(abc.ABCMeta)
class DirectiveNode(ParserNode):
"""
DirectiveNode class represents a configuration directive within the configuration.
It can have zero or more arguments attached to it. Because of the nature of
single directives, it is not able to have child nodes and hence it is always
treated as a leaf node.
"""
@property
@abc.abstractmethod
def enabled(self): # pragma: no cover
"""
Configuration blocks may have conditional statements enabling or disabling
their contents. This property returns the state of this DirectiveNode.
:returns: True if the DirectiveNode is parsed and enabled in the configuration.
:rtype: bool
"""
raise NotImplementedError
@property
@abc.abstractmethod
def name(self): # pragma: no cover
"""
Name property contains the name of the directive.
:returns: Name of this node
:rtype: str
"""
raise NotImplementedError
@property
@abc.abstractmethod
def parameters(self): # pragma: no cover
"""
This property contains a tuple of parameters of this ParserNode object
excluding whitespaces.
:returns: A tuple of parameters for this node
:rtype: tuple
"""
raise NotImplementedError
@abc.abstractmethod
def set_parameters(self, parameters):
"""
Sets the sequence of parameters for this ParserNode object without
whitespaces, and marks this object dirty.
:param list parameters: sequence of parameters
"""
@six.add_metaclass(abc.ABCMeta)
class BlockNode(ParserNode):
"""
BlockNode class represents a block of nested configuration directives, comments
and other blocks as its children. A BlockNode can have zero or more arguments
attached to it.
Configuration blocks typically consist of one or more child nodes of all possible
types. Because of this, the BlockNode class has various discovery and structure
management methods.
Lists of arguments used as an optional argument for some of the methods should
be lists of strings that are applicable arguments for each specific BlockNode
or DirectiveNode types. As an example, for a following configuration example:
<VirtualHost *:80>
...
</VirtualHost>
The node type would be BlockNode, name would be 'VirtualHost' and arguments
would be: ['*:80'].
While for the following example:
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
The node type would be DirectiveNode, name would be 'LoadModule' and arguments
would be: ['alias_module', '/usr/lib/apache2/modules/mod_alias.so']
The applicable arguments are dependent on the underlying configuration language
and its grammar.
"""
@abc.abstractmethod
def add_child_block(self, name, arguments=None, position=None):
"""
Adds a new BlockNode child node with provided values and marks the callee
BlockNode dirty. This is used to add new children to the AST.
:param str name: The name of the child node to add
:param list arguments: list of arguments for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: BlockNode instance of the created child block
"""
@abc.abstractmethod
def add_child_directive(self, name, arguments=None, position=None):
"""
Adds a new DirectiveNode child node with provided values and marks the
callee BlockNode dirty. This is used to add new children to the AST.
:param str name: The name of the child node to add
:param list arguments: list of arguments for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: DirectiveNode instance of the created child directive
"""
@abc.abstractmethod
def add_child_comment(self, comment="", position=None):
"""
Adds a new CommentNode child node with provided value and marks the
callee BlockNode dirty. This is used to add new children to the AST.
:param str comment: Comment contents
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: CommentNode instance of the created child comment
"""
@property
@abc.abstractmethod
def children(self): # pragma: no cover
"""
This property contains a list ParserNode objects that are the children
for this node. The order of children is the same as that of the parsed
configuration block.
:returns: A tuple of this block's children
:rtype: tuple
"""
raise NotImplementedError
@property
@abc.abstractmethod
def enabled(self):
"""
Configuration blocks may have conditional statements enabling or disabling
their contents. This property returns the state of this configuration block.
In case of unmatched conditional statement in block, this block itself should
be set enabled while its children should be set disabled.
:returns: True if the BlockNode is parsed and enabled in the configuration.
:rtype: bool
"""
@abc.abstractmethod
def find_blocks(self, name, exclude=True):
"""
Find a configuration block by name. This method walks the child tree of
ParserNodes under the instance it was called from. This way it is possible
to search for the whole configuration tree, when starting from root node or
to do a partial search when starting from a specified branch.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found BlockNode objects.
"""
@abc.abstractmethod
def find_directives(self, name, exclude=True):
"""
Find a directive by name. This method walks the child tree of ParserNodes
under the instance it was called from. This way it is possible to search
for the whole configuration tree, when starting from root node, or to do
a partial search when starting from a specified branch.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found DirectiveNode objects.
"""
@abc.abstractmethod
def find_comments(self, comment, exact=False):
"""
Find comments with value containing or being exactly the same as search term.
This method walks the child tree of ParserNodes under the instance it was
called from. This way it is possible to search for the whole configuration
tree, when starting from root node, or to do a partial search when starting
from a specified branch.
:param str comment: The content of comment to search for
:param bool exact: If the comment needs to exactly match the search term
:returns: A list of found CommentNode objects.
"""
@abc.abstractmethod
def delete_child(self, child):
"""
Remove a specified child node from the list of children of the called
BlockNode object.
:param ParserNode child: Child ParserNode object to remove from the list
of children of the callee.
"""
@property
@abc.abstractmethod
def name(self): # pragma: no cover
"""
Name property contains the name of the block. As an example for config:
<VirtualHost *:80> ... </VirtualHost>
the name would be "VirtualHost".
:returns: Name of this node
:rtype: str
"""
raise NotImplementedError
@property
@abc.abstractmethod
def parameters(self): # pragma: no cover
"""
This property contains a tuple of parameters of this ParserNode object
excluding whitespaces.
:returns: A tuple of parameters for this node
:rtype: tuple
"""
raise NotImplementedError
@abc.abstractmethod
def set_parameters(self, parameters):
"""
Sets the sequence of parameters for this ParserNode object without
whitespaces, and marks this object dirty.
:param list parameters: sequence of parameters
"""
@abc.abstractmethod
def unsaved_files(self):
"""
Returns a list of file paths that have been changed since the last save
(or the initial configuration parse). The intended use for this method
is to tell the Reverter which files need to be included in a checkpoint.
This is typically called for the root of the ParserNode tree.
:returns: list of file paths of files that have been changed but not yet
saved to disk.
"""

View File

@@ -61,6 +61,7 @@ class ApacheParser(object):
self.root = os.path.abspath(root)
self.loc = {"root": self._find_config_root()}
self.parse_file(self.loc["root"])
self.handle_includes()
if version >= (2, 4):
# Look up variables from httpd and add to DOM if not already parsed
@@ -269,6 +270,7 @@ class ApacheParser(object):
"""
mods = set() # type: Set[str]
self.handle_includes()
matches = self.find_dir("LoadModule")
iterator = iter(matches)
# Make sure prev_size != cur_size for do: while: iteration
@@ -321,11 +323,7 @@ class ApacheParser(object):
def update_includes(self):
"""Get includes from httpd process, and add them to DOM if needed"""
# Find_dir iterates over configuration for Include and IncludeOptional
# directives to make sure we see the full include tree present in the
# configuration files
_ = self.find_dir("Include")
self.handle_includes()
inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_INCLUDES"]
matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
@@ -606,6 +604,8 @@ class ApacheParser(object):
# includes = self.aug.match(start +
# "//* [self::directive='Include']/* [label()='arg']")
regex = "%s" % (case_i(directive))
regex = "(%s)|(%s)|(%s)" % (case_i(directive),
case_i("Include"),
case_i("IncludeOptional"))
@@ -637,6 +637,49 @@ class ApacheParser(object):
return ordered_matches
def handle_includes(self, known_matches=None):
"""
This method searches through the configuration tree for Include and
IncludeOptional directives and includes them in the Augeas DOM tree
recursively.
:param list known_matches: List of already known Augeas DOM paths of found
include directives.
"""
start = get_aug_path(self.loc["root"])
regex = "(%s)|(%s)" % (case_i("Include"), case_i("IncludeOptional"))
all_matches = self.aug.match(
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
if known_matches and all([True for m in all_matches if m in known_matches]):
# We already found everything previously
return
# Always use the exclusion as we want to have the correct state
matches = self._exclude_dirs(all_matches)
for match in matches:
incpath = self.get_arg(match + "/arg")
# Attempts to add a transform to the file if one does not already exist
# Standardize the include argument based on server root
if not incpath.startswith("/"):
# Normpath will condense ../
incpath = os.path.normpath(os.path.join(self.root, incpath))
else:
incpath = os.path.normpath(incpath)
if os.path.isdir(incpath):
self.parse_file(os.path.join(incpath, "*"))
else:
self.parse_file(incpath)
# Iterate. The results will be checked and the recursion broken when
# everything has been found.
self.handle_includes(all_matches)
def get_all_args(self, match):
"""
Tries to fetch all arguments for a directive. See get_arg.
@@ -755,10 +798,10 @@ class ApacheParser(object):
arg = os.path.normpath(arg)
# Attempts to add a transform to the file if one does not already exist
if os.path.isdir(arg):
self.parse_file(os.path.join(arg, "*"))
else:
self.parse_file(arg)
#if os.path.isdir(arg):
# self.parse_file(os.path.join(arg, "*"))
#else:
# self.parse_file(arg)
# Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex

View File

@@ -0,0 +1,84 @@
"""Runtime assertions for two different parser implementations"""
def simpleassert(old_value, new_value):
"""
Simple assertion
"""
assert old_value == new_value
def legacy_assert_list(old_list, new_list):
"""
Used to assert that both of the lists contain the same values.
"""
assert len(old_list) == len(new_list)
assert all([True for ol in old_list if ol in new_list])
def legacy_assert_dir(old_result, new_result):
"""
Used to test and ensure that the new implementation search results matches
the old implementation results. This test is intended to be used only to test
the old Augeas implementation results against the ParserNode Augeas implementation.
This test expects the DirectiveNode to have Augeas path in its attribute dict
"_metadata" with key "augpath".
"""
if new_result != "CERTBOT_PASS_ASSERT":
return
assert len(old_result) == len(new_result)
matching = []
for oldres in old_result:
match = [res for res in new_result if res._metadata["augpath"] == oldres]
assert len(match) == 1
def legacy_assert_args(old_result, new_result, parser):
"""
Used to test and ensure that returned parameter values are the same for
the old and the new implementation.
Uses ApacheParser to actually fetch the parameter for the old result.
This assertion is structured this way because of how parser.get_arg() is
currently used in the ApacheConfigurator, making it easier to test the
results.
"""
if isinstance(old_result, list):
for old in old_result:
oldarg = parser.get_arg(old_result)
assert oldarg in new_result.parameters
else:
oldarg = parser.get_arg(old_result)
assert oldarg in new_result.parameters
def assert_dir(first, second):
"""
Used to test that DirectiveNode results match for both implementations.
"""
if "CERTBOT_PASS_ASSERT" in [first, second]:
return
assert first.name == second.name
assert first.parameters == second.parameters
assert first.dirty == second.dirty
def assert_block(first, second):
"""
Used to test that BlockNode results match for both implementations.
"""
if "CERTBOT_PASS_ASSERT" in [first, second]:
return
assert first.name == second.name
assert first.parameters == second.parameters
assert len(first.children) == len(second.children)
assert first.dirty == second.dirty

View File

@@ -91,6 +91,7 @@ class ComplexParserTest(util.ParserTest):
from certbot_apache import parser
self.parser.add_dir(parser.get_aug_path(self.parser.loc["default"]),
"Include", [arg])
self.parser.handle_includes()
if hit:
self.assertTrue(self.parser.find_dir("FNMATCH_DIRECTIVE"))
else:

View File

@@ -0,0 +1,86 @@
""" Tests for ParserNode interface """
import unittest
from acme.magic_typing import Optional, Tuple # pylint: disable=unused-import, no-name-in-module
from certbot_apache import interfaces
class DummyCommentNode(interfaces.CommentNode):
""" A dummy class implementing CommentNode interface """
ancestor = None
comment = ""
dirty = False
def save(self, msg): # pragma: no cover
pass
class DummyDirectiveNode(interfaces.DirectiveNode):
""" A dummy class implementing DirectiveNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
dirty = False
enabled = True
name = ""
def save(self, msg): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
pass
class DummyBlockNode(interfaces.BlockNode):
""" A dummy class implementing BlockNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
children = tuple() # type: Tuple[interfaces.ParserNode, ...]
dirty = False
enabled = True
name = ""
def save(self, msg): # pragma: no cover
pass
def add_child_block(self, name, arguments=None, position=None): # pragma: no cover
pass
def add_child_directive(self, name, arguments=None, position=None): # pragma: no cover
pass
def add_child_comment(self, comment="", position=None): # pragma: no cover
pass
def find_blocks(self, name, exclude=True): # pragma: no cover
pass
def find_directives(self, name, exclude=True): # pragma: no cover
pass
def find_comments(self, comment, exact=False): # pragma: no cover
pass
def delete_child(self, child): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
pass
def unsaved_files(self): # pragma: no cover
pass
class ParserNodeTest(unittest.TestCase):
"""Dummy placeholder test case for ParserNode interfaces"""
def test_dummy(self):
dummyblock = DummyBlockNode()
dummydirective = DummyDirectiveNode()
dummycomment = DummyCommentNode()
if __name__ == "__main__":
unittest.main() # pragma: no cover