mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
293 lines
9.3 KiB
Python
293 lines
9.3 KiB
Python
import pytest
|
|
|
|
from sentry.discover.arithmetic import (
|
|
ArithmeticParseError,
|
|
ArithmeticValidationError,
|
|
MaxOperatorError,
|
|
Operation,
|
|
parse_arithmetic,
|
|
)
|
|
|
|
op_map = {
|
|
"+": "plus",
|
|
"-": "minus",
|
|
"*": "multiply",
|
|
"/": "divide",
|
|
"÷": "divide",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"a,op,b",
|
|
[
|
|
("12", "+", "34"),
|
|
(" 12 ", " + ", " 34 "),
|
|
("12", "-", "34"),
|
|
(" 12 ", " - ", " 34 "),
|
|
("12", "*", "34"),
|
|
(" 12 ", " * ", " 34 "),
|
|
("12", "/", "34"),
|
|
(" 12 ", " / ", " 34 "),
|
|
("12", "÷", "34"),
|
|
(" 12 ", " ÷ ", " 34 "),
|
|
("-12", "+", "-34"),
|
|
("+12", "+", "+34"),
|
|
("-12", "+", "+34"),
|
|
("+12", "+", "-34"),
|
|
(1.2345, "+", 6.7890),
|
|
(1.2345, "-", 6.7890),
|
|
(1.2345, "*", 6.7890),
|
|
(1.2345, "/", 6.7890),
|
|
(1.2345, "÷", 6.7890),
|
|
],
|
|
)
|
|
def test_simple_arithmetic(a, op, b):
|
|
equation = f"{a}{op}{b}"
|
|
result, _, _ = parse_arithmetic(equation)
|
|
assert result.operator == op_map[op.strip()], equation
|
|
assert result.lhs == float(a), equation
|
|
assert result.rhs == float(b), equation
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"a,op1,b,op2,c",
|
|
[
|
|
("12", "+", "34", "+", "56"),
|
|
("12", "+", "34", "-", "56"),
|
|
("12", "-", "34", "+", "56"),
|
|
("12", "-", "34", "-", "56"),
|
|
("12", "*", "34", "*", "56"),
|
|
("12", "*", "34", "/", "56"),
|
|
("12", "/", "34", "*", "56"),
|
|
("12", "/", "34", "/", "56"),
|
|
("12", "*", "34", "*", "56"),
|
|
("12", "*", "34", "÷", "56"),
|
|
("12", "÷", "34", "*", "56"),
|
|
("12", "÷", "34", "÷", "56"),
|
|
],
|
|
)
|
|
def test_homogenous_arithmetic(a, op1, b, op2, c):
|
|
"""Test that literal order of ops is respected assuming we don't have to worry about BEDMAS"""
|
|
equation = f"{a}{op1}{b}{op2}{c}"
|
|
result, _, _ = parse_arithmetic(equation)
|
|
assert result.operator == op_map[op2.strip()], equation
|
|
assert isinstance(result.lhs, Operation), equation
|
|
assert result.lhs.operator == op_map[op1.strip()], equation
|
|
assert result.lhs.lhs == float(a), equation
|
|
assert result.lhs.rhs == float(b), equation
|
|
assert result.rhs == float(c), equation
|
|
|
|
|
|
def test_mixed_arithmetic():
|
|
result, _, _ = parse_arithmetic("12 + 34 * 56")
|
|
assert result.operator == "plus"
|
|
assert result.lhs == 12.0
|
|
assert isinstance(result.rhs, Operation)
|
|
assert result.rhs.operator == "multiply"
|
|
assert result.rhs.lhs == 34.0
|
|
assert result.rhs.rhs == 56.0
|
|
|
|
result, _, _ = parse_arithmetic("12 / 34 - 56")
|
|
assert result.operator == "minus"
|
|
assert isinstance(result.lhs, Operation)
|
|
assert result.lhs.operator == "divide"
|
|
assert result.lhs.lhs == 12.0
|
|
assert result.lhs.rhs == 34.0
|
|
assert result.rhs == 56.0
|
|
|
|
|
|
def test_four_terms():
|
|
result, _, _ = parse_arithmetic("1 + 2 / 3 * 4")
|
|
assert result.operator == "plus"
|
|
assert result.lhs == 1.0
|
|
assert isinstance(result.rhs, Operation)
|
|
assert result.rhs.operator == "multiply"
|
|
assert isinstance(result.rhs.lhs, Operation)
|
|
assert result.rhs.lhs.operator == "divide"
|
|
assert result.rhs.lhs.lhs == 2.0
|
|
assert result.rhs.lhs.rhs == 3.0
|
|
assert result.rhs.rhs == 4.0
|
|
|
|
|
|
def test_brackets_with_two_inner_terms():
|
|
result, _, _ = parse_arithmetic("(1 + 2) / (3 - 4)")
|
|
assert result.operator == "divide"
|
|
assert isinstance(result.lhs, Operation)
|
|
assert result.lhs.operator == "plus"
|
|
assert result.lhs.lhs == 1.0
|
|
assert result.lhs.rhs == 2.0
|
|
assert isinstance(result.rhs, Operation)
|
|
assert result.rhs.operator == "minus"
|
|
assert result.rhs.lhs == 3.0
|
|
assert result.rhs.rhs == 4.0
|
|
|
|
|
|
def test_brackets_with_three_inner_terms():
|
|
result, _, _ = parse_arithmetic("(1 + 2 + 3) / 4")
|
|
assert result.operator == "divide"
|
|
assert isinstance(result.lhs, Operation)
|
|
assert result.lhs.operator == "plus"
|
|
assert isinstance(result.lhs.lhs, Operation)
|
|
assert result.lhs.lhs.lhs == 1.0
|
|
assert result.lhs.lhs.rhs == 2.0
|
|
assert result.lhs.rhs == 3.0
|
|
assert result.rhs == 4.0
|
|
|
|
|
|
def test_brackets_with_four_inner_terms():
|
|
result, _, _ = parse_arithmetic("(1 + 2 / 3 * 4)")
|
|
assert result.operator == "plus"
|
|
assert result.lhs == 1.0
|
|
assert isinstance(result.rhs, Operation)
|
|
assert result.rhs.operator == "multiply"
|
|
assert isinstance(result.rhs.lhs, Operation)
|
|
assert result.rhs.lhs.operator == "divide"
|
|
assert result.rhs.lhs.lhs == 2.0
|
|
assert result.rhs.lhs.rhs == 3.0
|
|
assert result.rhs.rhs == 4.0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"a,op1,b,op2,c,op3,d",
|
|
[
|
|
("12", "+", "34", "+", "56", "+", "78"),
|
|
("12", "-", "34", "-", "56", "-", "78"),
|
|
("12", "+", "34", "-", "56", "+", "78"),
|
|
("12", "-", "34", "+", "56", "-", "78"),
|
|
("12", "*", "34", "*", "56", "*", "78"),
|
|
("12", "/", "34", "/", "56", "/", "78"),
|
|
("12", "*", "34", "/", "56", "*", "78"),
|
|
("12", "/", "34", "*", "56", "/", "78"),
|
|
("12", "÷", "34", "÷", "56", "÷", "78"),
|
|
("12", "*", "34", "÷", "56", "*", "78"),
|
|
("12", "÷", "34", "*", "56", "÷", "78"),
|
|
],
|
|
)
|
|
def test_homogenous_four_terms(a, op1, b, op2, c, op3, d):
|
|
"""This basically tests flatten in the ArithmeticVisitor
|
|
|
|
flatten only kicks in when its a chain of the same operator type
|
|
"""
|
|
equation = f"{a}{op1}{b}{op2}{c}{op3}{d}"
|
|
result, _, _ = parse_arithmetic(equation)
|
|
assert result.operator == op_map[op3.strip()], equation
|
|
assert isinstance(result.lhs, Operation), equation
|
|
assert result.lhs.operator == op_map[op2.strip()], equation
|
|
assert isinstance(result.lhs.lhs, Operation), equation
|
|
assert result.lhs.lhs.operator == op_map[op1.strip()], equation
|
|
assert result.lhs.lhs.lhs == float(a), equation
|
|
assert result.lhs.lhs.rhs == float(b), equation
|
|
assert result.lhs.rhs == float(c), equation
|
|
assert result.rhs == float(d), equation
|
|
|
|
|
|
def test_max_operators():
|
|
with pytest.raises(MaxOperatorError):
|
|
parse_arithmetic("1 + 2 * 3 * 4", 2)
|
|
|
|
# exactly 3 should be ok
|
|
parse_arithmetic("1 + 2 * 3 * 4", 3)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"a,op,b",
|
|
[
|
|
("spans.http", "+", "spans.db"),
|
|
("transaction.duration", "*", 2),
|
|
(3.1415, "+", "spans.resource"),
|
|
],
|
|
)
|
|
def test_field_values(a, op, b):
|
|
for with_brackets in [False, True]:
|
|
equation = f"{a}{op}{b}"
|
|
if with_brackets:
|
|
equation = f"({equation}) + 5"
|
|
result, fields, functions = parse_arithmetic(equation)
|
|
if with_brackets:
|
|
assert result.operator == "plus"
|
|
assert isinstance(result.lhs, Operation)
|
|
assert result.rhs == 5.0
|
|
result = result.lhs
|
|
assert result.operator == op_map[op.strip()], equation
|
|
assert result.lhs == a, equation
|
|
assert result.rhs == b, equation
|
|
assert len(functions) == 0
|
|
if isinstance(a, str):
|
|
assert a in fields, equation
|
|
if isinstance(b, str):
|
|
assert b in fields, equation
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"lhs,op,rhs",
|
|
[
|
|
("failure_count()", "/", "count()"),
|
|
("percentile(0.5, transaction.duration)", "/", "max(transaction.duration)"),
|
|
(500, "+", "count_miserable(user, 300)"),
|
|
("count_miserable(user, 300)", "-", 500),
|
|
("p50(transaction.duration)", "/", "p100(transaction.duration)"),
|
|
("count_if(user.email,equals,[email protected])", "/", 2),
|
|
(100, "-", 'count_if(some_tag,notEquals,"something(really)annoying,like\\"this\\"")'),
|
|
],
|
|
)
|
|
def test_function_values(lhs, op, rhs):
|
|
for with_brackets in [False, True]:
|
|
equation = f"{lhs}{op}{rhs}"
|
|
if with_brackets:
|
|
equation = f"({equation}) + 5"
|
|
result, fields, functions = parse_arithmetic(equation)
|
|
if with_brackets:
|
|
assert result.operator == "plus"
|
|
assert isinstance(result.lhs, Operation)
|
|
assert result.rhs == 5.0
|
|
result = result.lhs
|
|
assert result.operator == op_map[op.strip()], equation
|
|
assert result.lhs == lhs, equation
|
|
assert result.rhs == rhs, equation
|
|
assert len(fields) == 0
|
|
if isinstance(lhs, str):
|
|
assert lhs in functions, equation
|
|
if isinstance(rhs, str):
|
|
assert rhs in functions, equation
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"equation",
|
|
[
|
|
"1 +",
|
|
"+ 1 + 1",
|
|
"1 + 1 +",
|
|
"1 ** 2",
|
|
"1 -- 1",
|
|
"hello world",
|
|
"",
|
|
"+",
|
|
],
|
|
)
|
|
def test_unparseable_arithmetic(equation):
|
|
with pytest.raises(ArithmeticParseError):
|
|
parse_arithmetic(equation)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"equation",
|
|
[
|
|
"5/0",
|
|
"spans.http/0",
|
|
# Transaction status isn't something we want arithmetic on
|
|
"1 + transaction.status",
|
|
# last_seen is an aggregate, but we aren't supporting arithmetic on dates
|
|
"last_seen() + 1",
|
|
# Mixing fields/functions is invalid
|
|
"p50(transaction.duration) + transaction.duration",
|
|
# Single fields are invalid cause there's no reason to use arithmetic for them
|
|
"12",
|
|
"p50(transaction.duration)",
|
|
"measurements.lcp",
|
|
"(measurements.lcp)",
|
|
],
|
|
)
|
|
def test_invalid_arithmetic(equation):
|
|
with pytest.raises(ArithmeticValidationError):
|
|
parse_arithmetic(equation)
|