aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild
diff options
context:
space:
mode:
authorXavier Claessens <xavier.claessens@collabora.com>2023-02-14 10:12:38 -0500
committerXavier Claessens <xclaesse@gmail.com>2023-03-01 20:13:34 -0500
commitf0dc61a76403d3ad26cbfa8e3922fa84343ba7d4 (patch)
tree7479c1af389b43a785c90a3a43e1574ff75d1205 /mesonbuild
parenta952b01a0885da9cc89b637d78ee649aea437a95 (diff)
downloadmeson-f0dc61a76403d3ad26cbfa8e3922fa84343ba7d4.zip
meson-f0dc61a76403d3ad26cbfa8e3922fa84343ba7d4.tar.gz
meson-f0dc61a76403d3ad26cbfa8e3922fa84343ba7d4.tar.bz2
interpreter: Add testcase..endtestcase clause support
This is currently only enabled when running unit tests to facilitate writing failing unit tests. Fixes: #11394
Diffstat (limited to 'mesonbuild')
-rw-r--r--mesonbuild/interpreter/interpreter.py22
-rw-r--r--mesonbuild/interpreterbase/__init__.py2
-rw-r--r--mesonbuild/interpreterbase/baseobjects.py5
-rw-r--r--mesonbuild/interpreterbase/interpreterbase.py13
-rw-r--r--mesonbuild/mparser.py20
5 files changed, 61 insertions, 1 deletions
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
index 2bf97da..9e20446 100644
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.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():
diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py
index 13f55e5..53dca49 100644
--- a/mesonbuild/interpreterbase/__init__.py
+++ b/mesonbuild/interpreterbase/__init__.py
@@ -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,
diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py
index 820e091..d5b8c94 100644
--- a/mesonbuild/interpreterbase/baseobjects.py
+++ b/mesonbuild/interpreterbase/baseobjects.py
@@ -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)
diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py
index da64f68..c8ef303 100644
--- a/mesonbuild/interpreterbase/interpreterbase.py
+++ b/mesonbuild/interpreterbase/interpreterbase.py
@@ -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:
diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py
index b0e817c..9840f9f 100644
--- a/mesonbuild/mparser.py
+++ b/mesonbuild/mparser.py
@@ -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: