Nemelis
Nemelis

Reputation: 5472

Pyparsing: Problem splitting up a check in 2 seperate parts, where it should continue the check with the 2nd part if the check on the first part fails

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:

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

Answers (1)

Nemelis
Nemelis

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

Related Questions