tests(pyargus): add tests for parsing expressions
This commit is contained in:
parent
019797f344
commit
5da441db42
9 changed files with 231 additions and 122 deletions
|
|
@ -16,6 +16,7 @@ dependencies:
|
||||||
- pytest
|
- pytest
|
||||||
- pytest-cov
|
- pytest-cov
|
||||||
- hypothesis
|
- hypothesis
|
||||||
|
- lark # hypothesis[lark]
|
||||||
- pip
|
- pip
|
||||||
- sphinx
|
- sphinx
|
||||||
- pydata-sphinx-theme
|
- pydata-sphinx-theme
|
||||||
|
|
|
||||||
26
noxfile.py
26
noxfile.py
|
|
@ -117,7 +117,7 @@ def mypy(session: nox.Session):
|
||||||
|
|
||||||
@nox.session
|
@nox.session
|
||||||
def tests(session: nox.Session):
|
def tests(session: nox.Session):
|
||||||
session.conda_install("pytest", "hypothesis")
|
session.conda_install("pytest", "hypothesis", "lark")
|
||||||
session.env.update(ENV)
|
session.env.update(ENV)
|
||||||
session.install("./pyargus")
|
session.install("./pyargus")
|
||||||
try:
|
try:
|
||||||
|
|
@ -127,14 +127,15 @@ def tests(session: nox.Session):
|
||||||
except Exception:
|
except Exception:
|
||||||
...
|
...
|
||||||
try:
|
try:
|
||||||
session.run("pytest", "pyargus")
|
with session.chdir(CURRENT_DIR / "pyargus"):
|
||||||
|
session.run("pytest", ".")
|
||||||
except Exception:
|
except Exception:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@nox.session
|
@nox.session
|
||||||
def coverage(session: nox.Session):
|
def coverage(session: nox.Session):
|
||||||
session.conda_install("pytest", "coverage", "hypothesis", "maturin", "lcov")
|
session.conda_install("pytest", "coverage", "hypothesis", "lark", "maturin", "lcov")
|
||||||
session.run("cargo", "install", "grcov", external=True, silent=True)
|
session.run("cargo", "install", "grcov", external=True, silent=True)
|
||||||
|
|
||||||
session.env.update(ENV)
|
session.env.update(ENV)
|
||||||
|
|
@ -181,15 +182,16 @@ def coverage(session: nox.Session):
|
||||||
...
|
...
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session.run(
|
with session.chdir(CURRENT_DIR / "pyargus"):
|
||||||
"coverage",
|
session.run(
|
||||||
"run",
|
"coverage",
|
||||||
"--source",
|
"run",
|
||||||
"pyargus/argus,pyargus/src",
|
"--source",
|
||||||
"-m",
|
"argus,src",
|
||||||
"pytest",
|
"-m",
|
||||||
silent=True,
|
"pytest",
|
||||||
)
|
silent=True,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ classifiers = [
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = ["typing-extensions"]
|
dependencies = ["typing-extensions"]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ dev = [
|
||||||
"black",
|
"black",
|
||||||
]
|
]
|
||||||
|
|
||||||
test = ["pytest", "pytest-cov", "hypothesis"]
|
test = ["pytest", "pytest-cov", "hypothesis[lark]"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["maturin>=1.0,<2.0"]
|
requires = ["maturin>=1.0,<2.0"]
|
||||||
|
|
|
||||||
0
pyargus/tests/__init__.py
Normal file
0
pyargus/tests/__init__.py
Normal file
16
pyargus/tests/test_expr.py
Normal file
16
pyargus/tests/test_expr.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from hypothesis import given
|
||||||
|
|
||||||
|
import argus
|
||||||
|
|
||||||
|
from .utils.expr_gen import argus_expr
|
||||||
|
|
||||||
|
|
||||||
|
@given(spec=argus_expr())
|
||||||
|
def test_correct_expr(spec: str) -> None:
|
||||||
|
try:
|
||||||
|
_ = argus.parse_expr(spec)
|
||||||
|
except ValueError as e:
|
||||||
|
logging.critical(f"unable to parse expr: {spec}")
|
||||||
|
raise e
|
||||||
|
|
@ -1,118 +1,18 @@
|
||||||
import typing
|
|
||||||
from typing import List, Tuple, Type, Union
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from hypothesis import assume, given
|
from hypothesis import assume, given
|
||||||
from hypothesis import strategies as st
|
from hypothesis import strategies as st
|
||||||
from hypothesis.strategies import SearchStrategy, composite
|
|
||||||
|
|
||||||
import argus
|
import argus
|
||||||
from argus import AllowedDtype, dtype
|
|
||||||
|
|
||||||
|
from .utils.signals_gen import (
|
||||||
def gen_element_fn(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[AllowedDtype]:
|
constant_signal,
|
||||||
new_dtype = dtype.convert(dtype_)
|
draw_index,
|
||||||
if new_dtype == dtype.bool_:
|
empty_signal,
|
||||||
return st.booleans()
|
gen_dtype,
|
||||||
elif new_dtype == dtype.int64:
|
gen_element_fn,
|
||||||
size = 2**64
|
gen_samples,
|
||||||
return st.integers(min_value=(-size // 2), max_value=((size - 1) // 2))
|
sampled_signal,
|
||||||
elif new_dtype == dtype.uint64:
|
)
|
||||||
size = 2**64
|
|
||||||
return st.integers(min_value=0, max_value=(size - 1))
|
|
||||||
elif new_dtype == dtype.float64:
|
|
||||||
return st.floats(
|
|
||||||
width=64,
|
|
||||||
allow_nan=False,
|
|
||||||
allow_infinity=False,
|
|
||||||
allow_subnormal=False,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"invalid dtype {dtype_}")
|
|
||||||
|
|
||||||
|
|
||||||
@composite
|
|
||||||
def gen_samples(
|
|
||||||
draw: st.DrawFn, min_size: int, max_size: int, dtype_: Union[Type[AllowedDtype], dtype]
|
|
||||||
) -> List[Tuple[float, AllowedDtype]]:
|
|
||||||
"""
|
|
||||||
Generate arbitrary samples for a signal where the time stamps are strictly
|
|
||||||
monotonically increasing
|
|
||||||
"""
|
|
||||||
elements = gen_element_fn(dtype_)
|
|
||||||
values = draw(st.lists(elements, min_size=min_size, max_size=max_size))
|
|
||||||
xs = draw(
|
|
||||||
st.lists(
|
|
||||||
st.integers(min_value=0, max_value=2**32 - 1),
|
|
||||||
unique=True,
|
|
||||||
min_size=len(values),
|
|
||||||
max_size=len(values),
|
|
||||||
)
|
|
||||||
.map(lambda t: map(float, sorted(set(t))))
|
|
||||||
.map(lambda t: list(zip(t, values)))
|
|
||||||
)
|
|
||||||
return xs
|
|
||||||
|
|
||||||
|
|
||||||
def empty_signal(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[argus.Signal]:
|
|
||||||
new_dtype: dtype = dtype.convert(dtype_)
|
|
||||||
sig: argus.Signal
|
|
||||||
if new_dtype == dtype.bool_:
|
|
||||||
sig = argus.BoolSignal()
|
|
||||||
assert sig.kind == dtype.bool_
|
|
||||||
elif new_dtype == dtype.uint64:
|
|
||||||
sig = argus.UnsignedIntSignal()
|
|
||||||
assert sig.kind == dtype.uint64
|
|
||||||
elif new_dtype == dtype.int64:
|
|
||||||
sig = argus.IntSignal()
|
|
||||||
assert sig.kind == dtype.int64
|
|
||||||
elif new_dtype == dtype.float64:
|
|
||||||
sig = argus.FloatSignal()
|
|
||||||
assert sig.kind == dtype.float64
|
|
||||||
else:
|
|
||||||
raise ValueError("unknown dtype")
|
|
||||||
return st.just(sig)
|
|
||||||
|
|
||||||
|
|
||||||
def constant_signal(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[argus.Signal]:
|
|
||||||
element = gen_element_fn(dtype_)
|
|
||||||
dtype_ = dtype.convert(dtype_)
|
|
||||||
if dtype_ == dtype.bool_:
|
|
||||||
return element.map(lambda val: argus.BoolSignal.constant(typing.cast(bool, val)))
|
|
||||||
if dtype_ == dtype.uint64:
|
|
||||||
return element.map(lambda val: argus.UnsignedIntSignal.constant(typing.cast(int, val)))
|
|
||||||
if dtype_ == dtype.int64:
|
|
||||||
return element.map(lambda val: argus.IntSignal.constant(typing.cast(int, val)))
|
|
||||||
if dtype_ == dtype.float64:
|
|
||||||
return element.map(lambda val: argus.FloatSignal.constant(typing.cast(float, val)))
|
|
||||||
raise ValueError("unsupported data type for signal")
|
|
||||||
|
|
||||||
|
|
||||||
def sampled_signal(xs: List[Tuple[float, AllowedDtype]], dtype_: Union[Type[AllowedDtype], dtype]) -> argus.Signal:
|
|
||||||
dtype_ = dtype.convert(dtype_)
|
|
||||||
if dtype_ == dtype.bool_:
|
|
||||||
return argus.BoolSignal.from_samples(typing.cast(List[Tuple[float, bool]], xs))
|
|
||||||
if dtype_ == dtype.uint64:
|
|
||||||
return argus.UnsignedIntSignal.from_samples(typing.cast(List[Tuple[float, int]], xs))
|
|
||||||
if dtype_ == dtype.int64:
|
|
||||||
return argus.IntSignal.from_samples(typing.cast(List[Tuple[float, int]], xs))
|
|
||||||
if dtype_ == dtype.float64:
|
|
||||||
return argus.FloatSignal.from_samples(typing.cast(List[Tuple[float, float]], xs))
|
|
||||||
raise ValueError("unsupported data type for signal")
|
|
||||||
|
|
||||||
|
|
||||||
@composite
|
|
||||||
def draw_index(draw: st.DrawFn, vec: List) -> int:
|
|
||||||
if len(vec) > 0:
|
|
||||||
return draw(st.integers(min_value=0, max_value=len(vec) - 1))
|
|
||||||
else:
|
|
||||||
return draw(st.just(0))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_dtype() -> SearchStrategy[Union[Type[AllowedDtype], dtype]]:
|
|
||||||
return st.one_of(
|
|
||||||
list(map(st.just, [dtype.bool_, dtype.uint64, dtype.int64, dtype.float64, bool, int, float])), # type: ignore[arg-type]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given(st.data())
|
@given(st.data())
|
||||||
|
|
|
||||||
0
pyargus/tests/utils/__init__.py
Normal file
0
pyargus/tests/utils/__init__.py
Normal file
73
pyargus/tests/utils/expr_gen.py
Normal file
73
pyargus/tests/utils/expr_gen.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"""Hypothesis strategies to generate Argus expressions
|
||||||
|
"""
|
||||||
|
import hypothesis.strategies as st
|
||||||
|
from hypothesis.extra.lark import from_lark
|
||||||
|
from lark import Lark, Transformer
|
||||||
|
|
||||||
|
|
||||||
|
class T(Transformer):
|
||||||
|
def INT(self, tok): # noqa: N802,ANN # type: ignore
|
||||||
|
"Convert the value of `tok` from string to int, while maintaining line number & column."
|
||||||
|
return tok.update(value=int(tok) // 2**64)
|
||||||
|
|
||||||
|
|
||||||
|
ARGUS_EXPR_GRAMMAR = Lark(
|
||||||
|
r"""
|
||||||
|
|
||||||
|
TRUE: "true" | "TRUE"
|
||||||
|
FALSE: "false" | "FALSE"
|
||||||
|
BOOLEAN: TRUE | FALSE
|
||||||
|
|
||||||
|
IDENT: ESCAPED_STRING | CNAME
|
||||||
|
|
||||||
|
num_expr: num_expr "*" num_expr
|
||||||
|
| num_expr "/" num_expr
|
||||||
|
| num_expr "+" num_expr
|
||||||
|
| num_expr "-" num_expr
|
||||||
|
| "-" num_expr
|
||||||
|
| NUMBER
|
||||||
|
| IDENT
|
||||||
|
| "(" num_expr ")"
|
||||||
|
|
||||||
|
cmp_expr: num_expr ">=" num_expr
|
||||||
|
| num_expr "<=" num_expr
|
||||||
|
| num_expr "<" num_expr
|
||||||
|
| num_expr ">" num_expr
|
||||||
|
| num_expr "==" num_expr
|
||||||
|
| num_expr "!=" num_expr
|
||||||
|
|
||||||
|
INTERVAL: "[" INT? "," INT? "]"
|
||||||
|
|
||||||
|
bool_expr: bool_expr "&&" bool_expr
|
||||||
|
| bool_expr "||" bool_expr
|
||||||
|
| bool_expr "<=>" bool_expr
|
||||||
|
| bool_expr "->" bool_expr
|
||||||
|
| bool_expr "^" bool_expr
|
||||||
|
| bool_expr WS_INLINE "U" WS_INLINE INTERVAL? bool_expr
|
||||||
|
| "!" bool_expr
|
||||||
|
| "G" WS_INLINE INTERVAL? bool_expr
|
||||||
|
| "F" WS_INLINE INTERVAL? bool_expr
|
||||||
|
| cmp_expr
|
||||||
|
| BOOLEAN
|
||||||
|
| IDENT
|
||||||
|
| "(" bool_expr ")"
|
||||||
|
|
||||||
|
phi: bool_expr
|
||||||
|
|
||||||
|
%import common.ESCAPED_STRING
|
||||||
|
%import common.CNAME
|
||||||
|
%import common.NUMBER
|
||||||
|
%import common.INT
|
||||||
|
%import common.WS
|
||||||
|
%import common.WS_INLINE
|
||||||
|
%ignore WS
|
||||||
|
|
||||||
|
""",
|
||||||
|
start="phi",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@st.composite
|
||||||
|
def argus_expr(draw: st.DrawFn) -> str:
|
||||||
|
"""Strategy to generate an Argus STL expression from a pre-defined grammar"""
|
||||||
|
return draw(from_lark(ARGUS_EXPR_GRAMMAR, start="phi"))
|
||||||
116
pyargus/tests/utils/signals_gen.py
Normal file
116
pyargus/tests/utils/signals_gen.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import typing
|
||||||
|
from typing import List, Tuple, Type, Union
|
||||||
|
|
||||||
|
from hypothesis import strategies as st
|
||||||
|
from hypothesis.strategies import SearchStrategy, composite
|
||||||
|
|
||||||
|
import argus
|
||||||
|
from argus import AllowedDtype, dtype
|
||||||
|
|
||||||
|
|
||||||
|
def gen_element_fn(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[AllowedDtype]:
|
||||||
|
new_dtype = dtype.convert(dtype_)
|
||||||
|
if new_dtype == dtype.bool_:
|
||||||
|
return st.booleans()
|
||||||
|
elif new_dtype == dtype.int64:
|
||||||
|
size = 2**64
|
||||||
|
return st.integers(min_value=(-size // 2), max_value=((size - 1) // 2))
|
||||||
|
elif new_dtype == dtype.uint64:
|
||||||
|
size = 2**64
|
||||||
|
return st.integers(min_value=0, max_value=(size - 1))
|
||||||
|
elif new_dtype == dtype.float64:
|
||||||
|
return st.floats(
|
||||||
|
width=64,
|
||||||
|
allow_nan=False,
|
||||||
|
allow_infinity=False,
|
||||||
|
allow_subnormal=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"invalid dtype {dtype_}")
|
||||||
|
|
||||||
|
|
||||||
|
@composite
|
||||||
|
def gen_samples(
|
||||||
|
draw: st.DrawFn,
|
||||||
|
min_size: int,
|
||||||
|
max_size: int,
|
||||||
|
dtype_: Union[Type[AllowedDtype], dtype],
|
||||||
|
) -> List[Tuple[float, AllowedDtype]]:
|
||||||
|
"""
|
||||||
|
Generate arbitrary samples for a signal where the time stamps are strictly
|
||||||
|
monotonically increasing
|
||||||
|
"""
|
||||||
|
elements = gen_element_fn(dtype_)
|
||||||
|
values = draw(st.lists(elements, min_size=min_size, max_size=max_size))
|
||||||
|
xs = draw(
|
||||||
|
st.lists(
|
||||||
|
st.integers(min_value=0, max_value=2**32 - 1),
|
||||||
|
unique=True,
|
||||||
|
min_size=len(values),
|
||||||
|
max_size=len(values),
|
||||||
|
)
|
||||||
|
.map(lambda t: map(float, sorted(set(t))))
|
||||||
|
.map(lambda t: list(zip(t, values)))
|
||||||
|
)
|
||||||
|
return xs
|
||||||
|
|
||||||
|
|
||||||
|
def empty_signal(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[argus.Signal]:
|
||||||
|
new_dtype: dtype = dtype.convert(dtype_)
|
||||||
|
sig: argus.Signal
|
||||||
|
if new_dtype == dtype.bool_:
|
||||||
|
sig = argus.BoolSignal()
|
||||||
|
assert sig.kind == dtype.bool_
|
||||||
|
elif new_dtype == dtype.uint64:
|
||||||
|
sig = argus.UnsignedIntSignal()
|
||||||
|
assert sig.kind == dtype.uint64
|
||||||
|
elif new_dtype == dtype.int64:
|
||||||
|
sig = argus.IntSignal()
|
||||||
|
assert sig.kind == dtype.int64
|
||||||
|
elif new_dtype == dtype.float64:
|
||||||
|
sig = argus.FloatSignal()
|
||||||
|
assert sig.kind == dtype.float64
|
||||||
|
else:
|
||||||
|
raise ValueError("unknown dtype")
|
||||||
|
return st.just(sig)
|
||||||
|
|
||||||
|
|
||||||
|
def constant_signal(dtype_: Union[Type[AllowedDtype], dtype]) -> SearchStrategy[argus.Signal]:
|
||||||
|
element = gen_element_fn(dtype_)
|
||||||
|
dtype_ = dtype.convert(dtype_)
|
||||||
|
if dtype_ == dtype.bool_:
|
||||||
|
return element.map(lambda val: argus.BoolSignal.constant(typing.cast(bool, val)))
|
||||||
|
if dtype_ == dtype.uint64:
|
||||||
|
return element.map(lambda val: argus.UnsignedIntSignal.constant(typing.cast(int, val)))
|
||||||
|
if dtype_ == dtype.int64:
|
||||||
|
return element.map(lambda val: argus.IntSignal.constant(typing.cast(int, val)))
|
||||||
|
if dtype_ == dtype.float64:
|
||||||
|
return element.map(lambda val: argus.FloatSignal.constant(typing.cast(float, val)))
|
||||||
|
raise ValueError("unsupported data type for signal")
|
||||||
|
|
||||||
|
|
||||||
|
def sampled_signal(xs: List[Tuple[float, AllowedDtype]], dtype_: Union[Type[AllowedDtype], dtype]) -> argus.Signal:
|
||||||
|
dtype_ = dtype.convert(dtype_)
|
||||||
|
if dtype_ == dtype.bool_:
|
||||||
|
return argus.BoolSignal.from_samples(typing.cast(List[Tuple[float, bool]], xs))
|
||||||
|
if dtype_ == dtype.uint64:
|
||||||
|
return argus.UnsignedIntSignal.from_samples(typing.cast(List[Tuple[float, int]], xs))
|
||||||
|
if dtype_ == dtype.int64:
|
||||||
|
return argus.IntSignal.from_samples(typing.cast(List[Tuple[float, int]], xs))
|
||||||
|
if dtype_ == dtype.float64:
|
||||||
|
return argus.FloatSignal.from_samples(typing.cast(List[Tuple[float, float]], xs))
|
||||||
|
raise ValueError("unsupported data type for signal")
|
||||||
|
|
||||||
|
|
||||||
|
@composite
|
||||||
|
def draw_index(draw: st.DrawFn, vec: List) -> int:
|
||||||
|
if len(vec) > 0:
|
||||||
|
return draw(st.integers(min_value=0, max_value=len(vec) - 1))
|
||||||
|
else:
|
||||||
|
return draw(st.just(0))
|
||||||
|
|
||||||
|
|
||||||
|
def gen_dtype() -> SearchStrategy[Union[Type[AllowedDtype], dtype]]:
|
||||||
|
return st.one_of(
|
||||||
|
list(map(st.just, [dtype.bool_, dtype.uint64, dtype.int64, dtype.float64, bool, int, float])), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue