File size: 5,390 Bytes
fb35807
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""
Derived-measure formulas must fail loudly, not generate zeros.

Regression: `_eval_formula` caught every exception and returned 0.0, and
`_eval_node` resolved an unknown identifier to 0.0. So a formula whose column
name did not match — `REVENUE - COGS` when the measures are actually
TOTAL_REVENUE / TOTAL_COGS — evaluated to `0 - 0 = 0` for every single row.

The column loaded 100% zero, the liveboard tile rendered blank, and the run
died much later in the post-load quality gate with no indication of WHICH
formula was wrong. Yodeck failed that gate with 12/12 dead derived columns and
no retry — the run just ended.

Formulas are now validated before a single row is generated, and every problem
is reported at once.

Run with: pytest tests/test_formula_validation.py -v
"""

import pytest

from demoprep_app.dataset.engine import (
    FormulaError,
    _eval_formula,
    validate_fact_formulas,
)
from demoprep_app.scenario.blueprint import BlueprintMeasure, FactSpec


def fact(*measures, name="FACT_SALES"):
    return FactSpec(name=name, grain="one row per order",
                    dimension_names=[], measures=list(measures))


def base(name):
    return BlueprintMeasure(name=name, kind="base")


def derived(name, formula):
    return BlueprintMeasure(name=name, kind="derived", formula=formula)


class TestTheYodeckCondition:
    def test_typod_measure_name_is_reported_not_zeroed(self):
        """The exact defect: formula names don't match the measures."""
        spec = fact(
            base("TOTAL_REVENUE"),
            base("TOTAL_COGS"),
            derived("GROSS_MARGIN", "REVENUE - COGS"),
        )
        with pytest.raises(FormulaError) as exc:
            validate_fact_formulas(spec)

        msg = str(exc.value)
        assert "GROSS_MARGIN" in msg
        assert "'REVENUE'" in msg
        assert "unknown" in msg

    def test_suggests_the_intended_measure(self):
        spec = fact(
            base("TOTAL_REVENUE"),
            derived("NET", "TOTAL_REVENUEE * 2"),
        )
        with pytest.raises(FormulaError) as exc:
            validate_fact_formulas(spec)
        assert "did you mean 'TOTAL_REVENUE'" in str(exc.value)

    def test_reports_every_problem_at_once(self):
        """Five typos must not cost five regenerate cycles."""
        spec = fact(
            base("A"),
            derived("D1", "NOPE1 + A"),
            derived("D2", "NOPE2 - A"),
            derived("D3", "NOPE3 * A"),
        )
        with pytest.raises(FormulaError) as exc:
            validate_fact_formulas(spec)
        msg = str(exc.value)
        assert "3 unusable derived measure formula(s)" in msg
        for name in ("NOPE1", "NOPE2", "NOPE3"):
            assert name in msg


class TestOtherStructuralFaults:
    def test_derived_without_formula(self):
        spec = fact(base("A"), derived("D", ""))
        with pytest.raises(FormulaError, match="declared derived but has no formula"):
            validate_fact_formulas(spec)

    def test_unparseable_formula(self):
        spec = fact(base("A"), derived("D", "A +"))
        with pytest.raises(FormulaError, match="does not parse"):
            validate_fact_formulas(spec)

    def test_function_call_rejected(self):
        spec = fact(base("A"), derived("D", "SUM(A)"))
        with pytest.raises(FormulaError, match="calls a function"):
            validate_fact_formulas(spec)

    def test_unsupported_operator_rejected(self):
        spec = fact(base("A"), derived("D", "A % 2"))
        with pytest.raises(FormulaError, match="unsupported operator"):
            validate_fact_formulas(spec)

    def test_comparison_rejected(self):
        spec = fact(base("A"), base("B"), derived("D", "A > B"))
        with pytest.raises(FormulaError, match="unsupported expression"):
            validate_fact_formulas(spec)


class TestValidFormulasPass:
    def test_simple_arithmetic(self):
        spec = fact(
            base("NET_SALES"),
            base("COST_AMOUNT"),
            derived("MARGIN", "NET_SALES - COST_AMOUNT"),
        )
        validate_fact_formulas(spec)  # must not raise

    def test_derived_referencing_derived(self):
        spec = fact(
            base("REVENUE"),
            base("COGS"),
            derived("MARGIN", "REVENUE - COGS"),
            derived("MARGIN_RATE", "MARGIN / REVENUE"),
        )
        validate_fact_formulas(spec)

    def test_constants_and_parens(self):
        spec = fact(base("A"), derived("D", "(A * 100) / 2 + 1"))
        validate_fact_formulas(spec)

    def test_base_measures_are_not_checked(self):
        """A base measure with a stray formula string is not a derived defect."""
        m = BlueprintMeasure(name="A", kind="base", formula="WHATEVER")
        validate_fact_formulas(fact(m))


class TestPerRowEvaluation:
    def test_division_by_zero_is_still_a_legitimate_zero(self):
        """Per-row arithmetic edge cases must stay tolerant."""
        assert _eval_formula("REVENUE / ORDERS", {"REVENUE": 100.0, "ORDERS": 0.0}) == 0.0

    def test_unknown_name_at_eval_time_raises(self):
        with pytest.raises(FormulaError, match="not an available measure"):
            _eval_formula("REVENUE - MISSING", {"REVENUE": 100.0})

    def test_normal_arithmetic(self):
        assert _eval_formula("A - B", {"A": 10.0, "B": 4.0}) == 6.0
        assert _eval_formula("-A", {"A": 3.0}) == -3.0