Reputation: 5472
For our client we have defined a domain-specific (Auto)test (SCript)-language to simplify setting up testcases.
Each test in an ASC-file consists out of three parts:
test <name> - <options> # <-- defines the start of a test and some general options
<testheader> # <-- contains a number of header commands which need to be always filled in
<testbody> # <-- the real test-actions
Both in a <testheader>
and <testbody>
whitelines are allowed (to make the testcases better readable)
To check if the definitions in these ASC-files are correct we have made a validator-script which checks the tests in the following way:
# definitions of valid_header_command and valid_test_command not listed here since they themselves are not of importance for the question (just lists with definitions of keywords for those particular sections)
anyotherline = restOfLine - Optional(LineEnd())
test_command = NotAny(OneOfKeywords('if', 'elif', 'else', 'fi') | eot) - (valid_header_command | valid_test_command | anyotherline)
block = Forward()
pre_post_block = Forward()
if_statement = Keyword('if') - vp_expression - eol
then_block = ZeroOrMore(block)
elif_block = Keyword('elif') - vp_expression - eol - ZeroOrMore(block)
else_block = Keyword('else') - eol - ZeroOrMore(block)
fi_statement = Keyword('fi') - eol
conditional_block = if_statement - then_block - ZeroOrMore(elif_block) - Optional(else_block) - fi_statement
block << ( OneOrMore(test_command) | conditional_block ) # pylint: disable=expression-not-assigned
test_implementation = (OneOrMore(block) + eot).setParseAction(self._parseaction_validate_mandatory_header_commands)
test_name = CharsNotIn(' +:!,?;@=()\n\r')
test_options = ( #option-definitios
)
test_definition = Keyword('test') - White(' ') - test_name.addParseAction(self._parseaction_validate_unique_testcase).addParseAction(self._parseaction_reset_per_testcase_data) - test_options - eol
# if we can't find a test_definition, but we can find a line with something on it (so not the end of file), then report an error
testcase = (test_definition - test_implementation) | (restOfLine + ~StringEnd() + LineEnd()).setParseAction(self._parseaction_errorExpectingNextTest)
This works for the biggest part, but we saw that some strange behaviour was happening when somebody put an if
around the <testheader>
commands to prevent having to code 2 testcases which only differ in the header.
After long deliberation we decided that an if
around the <testheader>
commands is not allowed, since it is very rare that only the <testheader>
differs.
So now we want to change the implementation in such a way that it does not allow if
statements around a <testheader>
anymore. To do this we wanted to try an approach like we did for testcase
where a seperate check for test_definition
(which defines the test
keyword) is used before the rest of the <testheader>
and <testbody>
are checked.
(Note: we must stay backwards compatible, since the if
around header-sections are almost never used).
What we tried was:
Split up the old test_command
in a header_command
section and a test_command
section (only the changed code from the snippet above):
header_command = NotAny(OneOfKeywords('if', 'elif', 'else', 'fi') | eot) - (valid_header_command)
test_command = NotAny(OneOfKeywords('if', 'elif', 'else', 'fi') | eot) - (valid_test_command | anyotherline)
....
test_implementation = OneOrMore(header_command).setParseAction(self._parseaction_validate_mandatory_header_commands) + OneOrMore(block) + eot
For the <testheader>
commands this solution is working. But now it fails on every <testbody>
command since they do not match with the header_command
section, where we would like that it continues with the test_command
section if it fails in the header_command
section.
Note again: whitespaces are allowed both in header- and body-sections, so we cannot use those as delimiters. And we must stay backwards compatible, so it is difficult/impossible to introduce any other delimiter.
We also tried keeping the original code but adding checks to the valid_header_command
section, but that does not work since although the conditional_block
definition is part of block
it also contains block
and thus only when the parts of if
statements are already handled it will handle the remaining test_command
part which contains the check in valid_header_command
. So handling it there is Just Too Late.
And lastly: We considered changing the _parseaction_validate_mandatory_header_commands
method, but how can we make sure that when that fails it goes first to the test_command
before really raising an error?
Hence we did not follow that approach further at the moment.
We think that our original approach of splitting up the old testcommand
into 2 sections is the correct one, but we are already breaking our heads for a number of days on this to get it to work. So we end up here asking for help.
--> Does anybody have an idea how we can make sure that after our validator sees that it is not a <testheader>
command it continues to check against the <testbody>
commands before raising an error?
Note: implementation is done in python 2.7 with pyparsing 2.3.0
Upvotes: 1
Views: 72
Reputation: 5472
A colleague of me found a working solution.
He also split up the block into a part that includes all and a test-command only part and replaced the block sections in the if-stements with the test-command only block. He also added some extra parseactions:
test_command = NotAny(conditional_construct | eot) - (valid_header_command | valid_test_command | anyotherline)
no_header_command = NotAny(conditional_construct | eot) - (valid_test_command | anyotherline)
block = Forward()
no_header_block = Forward()
if_statement = (Keyword('if') - vp_expression - eol).addParseAction(self._parseaction_in_if_statement)
then_block = ZeroOrMore(no_header_block)
elif_block = Keyword('elif') - vp_expression - eol - ZeroOrMore(no_header_block)
else_block = Keyword('else') - eol - ZeroOrMore(no_header_block)
fi_statement = (Keyword('fi') - eol).addParseAction(self._parseaction_out_if_statement)
conditional_block = if_statement - then_block - ZeroOrMore(elif_block) - Optional(else_block) - fi_statement
block << ( OneOrMore(test_command) | conditional_block ) # pylint: disable=expression-not-assigned
no_header_block << ( OneOrMore(no_header_command) | conditional_block ) # pylint: disable=expression-not-assigned
Upvotes: 1