diff --git a/snowcap/blueprint.py b/snowcap/blueprint.py index 6912d25..2a287bb 100644 --- a/snowcap/blueprint.py +++ b/snowcap/blueprint.py @@ -4,6 +4,7 @@ from collections import defaultdict from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass +from itertools import zip_longest from typing import ( Any, Generator, @@ -887,6 +888,29 @@ def _summarize_plan_value(value, max_len: int = 60) -> str: return f"<{lines} {unit}, {len(text)} chars>" +def _plan_value_rows(key: str, before, after) -> list[list[str]]: + """Rows of the plan table for one changed property. + + A list is shown one item per row, before and after side by side, so the plan names the + items that changed. As one value, a list of IP addresses or triggers soon outgrows the + table width and collapses to its length. A dict item is shown as `key: value` pairs, and + an item is shown in full however long it is: only a multiline item collapses, because the + table cannot lay out a newline. + """ + if isinstance(before, list) or isinstance(after, list): + pairs = list(zip_longest(map(_plan_item, before or []), map(_plan_item, after or []), fillvalue="")) + if pairs: + return [[key if index == 0 else "", old, new] for index, (old, new) in enumerate(pairs)] + return [[key, _summarize_plan_value(before), _summarize_plan_value(after)]] + + +def _plan_item(item) -> str: + if isinstance(item, dict): + item = ", ".join(f"{field}: {value}" for field, value in item.items()) + text = str(item) + return _summarize_plan_value(text) if "\n" in text else text + + def _render_table(rows: list[list[str]], headers: list[str]) -> str: """ Render a table with box-drawing characters. @@ -1191,14 +1215,7 @@ def _dump_plan_text(plan: Plan) -> str: for key, new_value in change.delta.items(): if key.startswith("_"): continue - before = change.before.get(key, "") - rows.append( - [ - key, - _summarize_plan_value(before), - _summarize_plan_value(new_value), - ] - ) + rows.extend(_plan_value_rows(key, change.before.get(key, ""), new_value)) if rows: table = _render_table(rows, ["Property", "Before", "After"]) diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py index 40aa49f..0df04da 100644 --- a/tests/test_blueprint.py +++ b/tests/test_blueprint.py @@ -2849,6 +2849,54 @@ def test_tables_are_listed_from_account_usage_when_it_is_on(self): assert not any("SHOW TABLES IN ACCOUNT" in statement for statement in sql) +@pytest.mark.parametrize( + "resource_cls, urn, key, before, after, expected_rows", + [ + ( + res.NetworkPolicy, + "urn::ABCD123:network_policy/OFFICE", + "allowed_ip_list", + ["10.0.0.1/32", "10.0.0.2/32", "10.0.0.3/32", "10.0.0.4/32", "10.0.0.5/32"], + ["10.0.0.1/32", "10.0.0.2/32", "10.0.0.3/32", "10.0.0.4/32", "10.0.0.6/32"], + [ + "│ allowed_ip_list │ 10.0.0.1/32 │ 10.0.0.1/32 │", + "│ │ 10.0.0.2/32 │ 10.0.0.2/32 │", + "│ │ 10.0.0.3/32 │ 10.0.0.3/32 │", + "│ │ 10.0.0.4/32 │ 10.0.0.4/32 │", + "│ │ 10.0.0.5/32 │ 10.0.0.6/32 │", + ], + ), + ( + res.HybridTable, + "urn::ABCD123:hybrid_table/DB.SCH.LISTINGS", + "indexes", + [{"name": "IDX_CITY", "columns": ["CITY"]}], + [ + {"name": "IDX_CITY", "columns": ["CITY"]}, + {"name": "IDX_REGION", "columns": ["STATE", "COUNTY", "CITY", "POSTAL_CODE"]}, + ], + [ + "│ indexes │ name: IDX_CITY, columns: ['CITY'] │ name: IDX_CITY, columns: ['CITY'] │", + "│ │ │ name: IDX_REGION, columns: ['STATE', 'COUNTY', 'CITY', 'POSTAL_CODE'] │", + ], + ), + ], +) +def test_plan_text_shows_each_item_of_a_changed_list(resource_cls, urn, key, before, after, expected_rows): + """A list longer than the table's value width is shown one item per row, so the plan says + which items changed instead of only how long the list is.""" + change = UpdateResource( + urn=parse_URN(urn), + resource_cls=resource_cls, + before={key: before}, + after={key: after}, + delta={key: after}, + ) + plan_lines = [line.strip() for line in strip_ansi(dump_plan([change], format="text")).splitlines()] + start = plan_lines.index(expected_rows[0]) + assert plan_lines[start : start + len(expected_rows)] == expected_rows + + class TestSummarizePlanValue: """The plan table must not dump a multiline SQL body (alert THEN, task body)."""