interpreter: Add testcase..endtestcase clause support

This is currently only enabled when running unit tests to facilitate
writing failing unit tests.

Fixes: #11394
pull/11481/head
Xavier Claessens 2 years ago committed by Xavier Claessens
parent a952b01a08
commit f0dc61a764
  1. 22
      mesonbuild/interpreter/interpreter.py
  2. 2
      mesonbuild/interpreterbase/__init__.py
  3. 5
      mesonbuild/interpreterbase/baseobjects.py
  4. 13
      mesonbuild/interpreterbase/interpreterbase.py
  5. 20
      mesonbuild/mparser.py
  6. 37
      test cases/common/261 testcase clause/meson.build
  7. 9
      test cases/common/261 testcase clause/test.json
  8. 6
      unittests/datatests.py

@ -33,7 +33,7 @@ from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, noArgsFlatte
from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest
from ..interpreterbase import Disabler, disablerIfNotFound
from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs, FeatureDeprecatedKwargs
from ..interpreterbase import ObjectHolder
from ..interpreterbase import ObjectHolder, ContextManagerObject
from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule
from ..cmake import CMakeInterpreter
from ..backend.backends import ExecutableSerialisation
@ -416,6 +416,8 @@ class Interpreter(InterpreterBase, HoldableObject):
})
if 'MESON_UNIT_TEST' in os.environ:
self.funcs.update({'exception': self.func_exception})
if 'MESON_RUNNING_IN_PROJECT_TESTS' in os.environ:
self.funcs.update({'expect_error': self.func_expect_error})
def build_holder_map(self) -> None:
'''
@ -1395,6 +1397,24 @@ class Interpreter(InterpreterBase, HoldableObject):
def func_exception(self, node, args, kwargs):
raise RuntimeError('unit test traceback :)')
@noKwargs
@typed_pos_args('expect_error', str)
def func_expect_error(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: TYPE_kwargs) -> ContextManagerObject:
class ExpectErrorObject(ContextManagerObject):
def __init__(self, msg: str, subproject: str) -> None:
super().__init__(subproject)
self.msg = msg
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val is None:
raise InterpreterException('Expecting an error but code block succeeded')
if isinstance(exc_val, mesonlib.MesonException):
msg = str(exc_val)
if msg != self.msg:
raise InterpreterException(f'Expecting error {self.msg!r} but got {msg!r}')
return True
return ExpectErrorObject(args[0], self.subproject)
def add_languages(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> bool:
success = self.add_languages_for(args, required, for_machine)
if not self.coredata.is_cross_build():

@ -18,6 +18,7 @@ __all__ = [
'ObjectHolder',
'IterableObject',
'MutableInterpreterObject',
'ContextManagerObject',
'MesonOperator',
@ -80,6 +81,7 @@ from .baseobjects import (
ObjectHolder,
IterableObject,
MutableInterpreterObject,
ContextManagerObject,
TV_fw_var,
TV_fw_args,

@ -22,6 +22,7 @@ import textwrap
import typing as T
from abc import ABCMeta
from contextlib import AbstractContextManager
if T.TYPE_CHECKING:
from typing_extensions import Protocol
@ -180,3 +181,7 @@ class IterableObject(metaclass=ABCMeta):
def size(self) -> int:
raise MesonBugException(f'size not implemented for {self.__class__.__name__}')
class ContextManagerObject(MesonInterpreterObject, AbstractContextManager):
def __init__(self, subproject: 'SubProject') -> None:
super().__init__(subproject=subproject)

@ -26,6 +26,7 @@ from .baseobjects import (
InterpreterObjectTypeVar,
ObjectHolder,
IterableObject,
ContextManagerObject,
HoldableTypes,
)
@ -231,6 +232,8 @@ class InterpreterBase:
raise ContinueRequest()
elif isinstance(cur, mparser.BreakNode):
raise BreakRequest()
elif isinstance(cur, mparser.TestCaseClauseNode):
return self.evaluate_testcase(cur)
else:
raise InvalidCode("Unknown statement.")
return None
@ -294,6 +297,16 @@ class InterpreterBase:
self.evaluate_codeblock(node.elseblock)
return None
def evaluate_testcase(self, node: mparser.TestCaseClauseNode) -> T.Optional[Disabler]:
result = self.evaluate_statement(node.condition)
if isinstance(result, Disabler):
return result
if not isinstance(result, ContextManagerObject):
raise InvalidCode(f'testcase clause {result!r} does not evaluate to a context manager.')
with result:
self.evaluate_codeblock(node.block)
return None
def evaluate_comparison(self, node: mparser.ComparisonNode) -> InterpreterObject:
val1 = self.evaluate_statement(node.left)
if val1 is None:

@ -16,6 +16,7 @@ from __future__ import annotations
from dataclasses import dataclass
import re
import codecs
import os
import typing as T
from .mesonlib import MesonException
from . import mlog
@ -113,6 +114,9 @@ class Lexer:
'endif', 'and', 'or', 'not', 'foreach', 'endforeach',
'in', 'continue', 'break'}
self.future_keywords = {'return'}
self.in_unit_test = 'MESON_RUNNING_IN_PROJECT_TESTS' in os.environ
if self.in_unit_test:
self.keywords.update({'testcase', 'endtestcase'})
self.token_specification = [
# Need to be sorted longest to shortest.
('ignore', re.compile(r'[ \t]')),
@ -466,6 +470,12 @@ class IfClauseNode(BaseNode):
self.ifs = [] # type: T.List[IfNode]
self.elseblock = None # type: T.Union[EmptyNode, CodeBlockNode]
class TestCaseClauseNode(BaseNode):
def __init__(self, condition: BaseNode, block: CodeBlockNode):
super().__init__(condition.lineno, condition.colno, condition.filename)
self.condition = condition
self.block = block
class UMinusNode(BaseNode):
def __init__(self, current_location: Token, value: BaseNode):
super().__init__(current_location.lineno, current_location.colno, current_location.filename)
@ -808,6 +818,12 @@ class Parser:
return self.codeblock()
return EmptyNode(self.current.lineno, self.current.colno, self.current.filename)
def testcaseblock(self) -> TestCaseClauseNode:
condition = self.statement()
self.expect('eol')
block = self.codeblock()
return TestCaseClauseNode(condition, block)
def line(self) -> BaseNode:
block_start = self.current
if self.current == 'eol':
@ -824,6 +840,10 @@ class Parser:
return ContinueNode(self.current)
if self.accept('break'):
return BreakNode(self.current)
if self.lexer.in_unit_test and self.accept('testcase'):
block = self.testcaseblock()
self.block_expect('endtestcase', block_start)
return block
return self.statement()
def codeblock(self) -> CodeBlockNode:

@ -0,0 +1,37 @@
project('testcase clause')
# To make sure unreachable code is not executed.
unreachable = true
# Verify assertion exception gets catched and dropped.
testcase expect_error('Assert failed: false')
assert(false)
unreachable = false
endtestcase
assert(unreachable)
# The inner testcase raises an exception because it did not receive the expected
# error message. The outer testcase catches the inner testcase exception and
# drop it.
testcase expect_error('Expecting error \'something\' but got \'Assert failed: false\'')
testcase expect_error('something')
assert(false)
unreachable = false
endtestcase
unreachable = false
endtestcase
assert(unreachable)
# The inner testcase raises an exception because it did not receive an
# exception. The outer testcase catches the inner testcase exception and
# drop it.
testcase expect_error('Expecting an error but code block succeeded')
testcase expect_error('something')
reached = true
endtestcase
unreachable = false
endtestcase
assert(reached)
assert(unreachable)
message('all good')

@ -0,0 +1,9 @@
{
"stdout": [
{
"line": ".*all good",
"match": "re",
"count": 1
}
]
}

@ -219,11 +219,14 @@ class DataTests(unittest.TestCase):
name = name.replace('_', '-')
self.assertIn(name, html)
@unittest.mock.patch.dict(os.environ)
def test_vim_syntax_highlighting(self):
'''
Ensure that vim syntax highlighting files were updated for new
functions in the global namespace in build files.
'''
# Disable unit test specific syntax
del os.environ['MESON_RUNNING_IN_PROJECT_TESTS']
env = get_fake_env()
interp = Interpreter(FakeBuild(env), mock=True)
with open('data/syntax-highlighting/vim/syntax/meson.vim', encoding='utf-8') as f:
@ -231,11 +234,14 @@ class DataTests(unittest.TestCase):
defined = set([a.strip() for a in res.group().split('\\')][1:])
self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys())))
@unittest.mock.patch.dict(os.environ)
def test_all_functions_defined_in_ast_interpreter(self):
'''
Ensure that the all functions defined in the Interpreter are also defined
in the AstInterpreter (and vice versa).
'''
# Disable unit test specific syntax
del os.environ['MESON_RUNNING_IN_PROJECT_TESTS']
env = get_fake_env()
interp = Interpreter(FakeBuild(env), mock=True)
astint = AstInterpreter('.', '', '')

Loading…
Cancel
Save