Files
superset2/tests/unit_tests/versioning/test_normalization_changes.py
T

187 lines
6.2 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from sqlalchemy.orm import Session
from superset.versioning.changes.normalization import (
consume_normalization_context,
filter_normalization_records,
matching_normalization_context,
MAX_NORMALIZATION_TRANSITIONS,
NormalizationContext,
NormalizationTransition,
sanitize_normalization_changes,
store_normalization_context,
)
from superset.versioning.diff import ChangeRecord
def _transition(
control: str = "row_limit", from_value: object = None, to_value: object = 10000
) -> dict[str, object]:
return {
"control": control,
"from_present": True,
"from_value": from_value,
"to_present": True,
"to_value": to_value,
}
def test_sanitizer_preserves_missing_and_null_as_distinct_states() -> None:
missing: dict[str, object] = {
"control": "show_legend",
"from_present": False,
"to_present": True,
"to_value": True,
}
null: dict[str, object] = _transition("row_limit")
transitions: tuple[NormalizationTransition, ...] = sanitize_normalization_changes(
[missing, null]
)
assert len(transitions) == 2
assert not transitions[0].from_present
assert transitions[1].from_present
assert transitions[1].from_value is None
def test_sanitizer_ignores_bad_entries_but_rejects_duplicate_envelope() -> None:
assert len(sanitize_normalization_changes([{"bad": True}, _transition()])) == 1
assert sanitize_normalization_changes([_transition(), _transition()]) == ()
assert sanitize_normalization_changes({"not": "a list"}) == ()
def test_sanitizer_rejects_bounded_envelope_failures() -> None:
too_many: list[dict[str, object]] = [
_transition(control=f"control_{index}")
for index in range(MAX_NORMALIZATION_TRANSITIONS + 1)
]
deep_value: object = None
for _index in range(22):
deep_value = [deep_value]
assert sanitize_normalization_changes(too_many) == ()
assert sanitize_normalization_changes([_transition(control="x" * 257)]) == ()
assert sanitize_normalization_changes([_transition(from_value=deep_value)]) == ()
assert sanitize_normalization_changes([object()]) == ()
def test_matching_requires_exact_presence_and_json_value_types() -> None:
raw: list[dict[str, object]] = [
{
"control": "show_legend",
"from_present": False,
"to_present": True,
"to_value": True,
},
_transition(),
]
context: NormalizationContext | None = matching_normalization_context(
7, raw, {"row_limit": None}, {"show_legend": True, "row_limit": 10000}
)
assert context is not None
assert {item.control for item in context.transitions} == {
"show_legend",
"row_limit",
}
assert (
matching_normalization_context(
7,
[_transition(from_value=True, to_value=2)],
{"row_limit": 1},
{"row_limit": 2},
)
is None
)
def test_filter_returns_fresh_records_without_matching_params_control() -> None:
records: list[ChangeRecord] = [
ChangeRecord("field", "edit", ["params", "row_limit"], None, 10000),
ChangeRecord("field", "edit", ["slice_name"], "Old", "New"),
]
context: NormalizationContext | None = matching_normalization_context(
7, [_transition()], {"row_limit": None}, {"row_limit": 10000}
)
filtered: list[ChangeRecord] = filter_normalization_records(records, context)
assert filtered == [records[1]]
assert filtered is not records
def test_context_is_consumed_once_and_same_chart_ambiguity_fails_open() -> None:
session: Session = Session()
context: NormalizationContext | None = matching_normalization_context(
7, [_transition()], {"row_limit": None}, {"row_limit": 10000}
)
assert context is not None
store_normalization_context(session, context)
assert consume_normalization_context(session, 7) == context
assert consume_normalization_context(session, 7) is None
store_normalization_context(session, context)
store_normalization_context(session, context)
assert consume_normalization_context(session, 7) is None
def test_drop_transition_matches_and_filters_a_remove_record() -> None:
"""A stash-time drop (present -> absent) suppresses its remove record."""
raw: list[dict[str, object]] = [
{
"control": "order_desc",
"from_present": True,
"from_value": True,
"to_present": False,
},
]
context: NormalizationContext | None = matching_normalization_context(
7, raw, {"order_desc": True, "row_limit": 100}, {"row_limit": 100}
)
assert context is not None
assert [item.control for item in context.transitions] == ["order_desc"]
records: list[ChangeRecord] = [
ChangeRecord("field", "remove", ["params", "order_desc"], True, None),
ChangeRecord("field", "edit", ["params", "row_limit"], 100, 50),
]
assert filter_normalization_records(records, context) == [records[1]]
def test_drop_transition_requires_the_key_to_be_absent_after() -> None:
"""A drop advisory does not match when the key survived the save."""
raw: list[dict[str, object]] = [
{
"control": "order_desc",
"from_present": True,
"from_value": True,
"to_present": False,
},
]
assert (
matching_normalization_context(
7, raw, {"order_desc": True}, {"order_desc": False}
)
is None
)