b8d13f5572
declare -A / local -A / readonly -A build a real map: keyed and pair literals, element and += writes, unset of one key, declare -p rendering with GNU's trailing space and key quoting, kind conversion refusals, [[ -v ]] on elements, and read/printf -v into an element. Arithmetic gains element lvalues (a[i]++, m[k]+=2) through an ElementOps seam the session fills, so $((a[i])) and ((m[k]=5)) resolve without the evaluator learning what a session is. Two pinned divergences: keys walk in sorted order, and a bare declare -A prints =(). Pinned against bash 5.2.37; 49 unit cases per language, 15 battery cases on 22 targets, both hosts byte-identical.
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
from mirage.shell.array import (array_append, array_count, array_extent,
|
|
array_get, array_has, array_indices, array_set,
|
|
array_slice, array_unset, array_values,
|
|
build_assoc_literal, build_indexed_literal,
|
|
keyed_word, make_array)
|
|
|
|
|
|
def test_make_array_is_dense_from_zero():
|
|
arr = make_array(["a", "b"])
|
|
assert arr == ["a", "b"]
|
|
assert array_indices(arr) == [0, 1]
|
|
|
|
|
|
def test_set_pads_with_holes_not_empty_strings():
|
|
arr: list[str | None] = []
|
|
array_set(arr, 3, "v")
|
|
assert arr == [None, None, None, "v"]
|
|
# The hole is addressable but is not an element.
|
|
assert array_count(arr) == 1
|
|
assert array_values(arr) == ["v"]
|
|
assert array_indices(arr) == [3]
|
|
assert array_extent(arr) == 4
|
|
assert array_get(arr, 0) == ""
|
|
assert not array_has(arr, 0)
|
|
assert array_has(arr, 3)
|
|
|
|
|
|
def test_unset_interior_element_keeps_later_indices():
|
|
arr = make_array(["zero", "one", "two"])
|
|
array_unset(arr, 1)
|
|
assert arr == ["zero", None, "two"]
|
|
assert array_get(arr, 2) == "two"
|
|
assert array_count(arr) == 2
|
|
assert array_indices(arr) == [0, 2]
|
|
|
|
|
|
def test_unset_trailing_element_shrinks_the_extent():
|
|
arr = make_array(["x", "y", "z"])
|
|
array_unset(arr, 2)
|
|
assert arr == ["x", "y"]
|
|
assert array_extent(arr) == 2
|
|
|
|
|
|
def test_unset_trailing_element_drops_earlier_holes_too():
|
|
arr = make_array(["x", "y", "z"])
|
|
array_unset(arr, 1)
|
|
array_unset(arr, 2)
|
|
assert arr == ["x"]
|
|
|
|
|
|
def test_unset_out_of_range_is_a_no_op():
|
|
arr = make_array(["x"])
|
|
array_unset(arr, 5)
|
|
array_unset(arr, -1)
|
|
assert arr == ["x"]
|
|
|
|
|
|
def test_append_starts_at_the_extent():
|
|
arr = make_array(["x", "y", "z"])
|
|
array_unset(arr, 1)
|
|
array_append(arr, ["w"])
|
|
assert arr == ["x", None, "z", "w"]
|
|
assert array_indices(arr) == [0, 2, 3]
|
|
|
|
|
|
def test_append_refills_a_trailing_hole():
|
|
arr = make_array(["x", "y", "z"])
|
|
array_unset(arr, 2)
|
|
array_append(arr, ["w"])
|
|
assert arr == ["x", "y", "w"]
|
|
|
|
|
|
def test_set_reassigns_a_hole():
|
|
arr = make_array(["x", "y", "z"])
|
|
array_unset(arr, 1)
|
|
array_set(arr, 1, "new")
|
|
assert arr == ["x", "new", "z"]
|
|
assert array_count(arr) == 3
|
|
|
|
|
|
def test_empty_string_is_an_element_not_a_hole():
|
|
arr = make_array(["", "y"])
|
|
assert array_count(arr) == 2
|
|
assert array_indices(arr) == [0, 1]
|
|
assert array_values(arr) == ["", "y"]
|
|
|
|
|
|
def test_slice_keeps_subscripts_not_ordinals():
|
|
arr: list[str | None] = []
|
|
array_set(arr, 1, "b")
|
|
array_set(arr, 3, "d")
|
|
array_set(arr, 9, "j")
|
|
# Offset 2 means "index >= 2", not "skip the first two values".
|
|
assert array_slice(arr, 2, None) == ["d", "j"]
|
|
assert array_slice(arr, 0, None) == ["b", "d", "j"]
|
|
assert array_slice(arr, 4, None) == ["j"]
|
|
assert array_slice(arr, 20, None) == []
|
|
|
|
|
|
def test_slice_length_counts_elements_taken():
|
|
arr: list[str | None] = []
|
|
array_set(arr, 1, "b")
|
|
array_set(arr, 3, "d")
|
|
array_set(arr, 9, "j")
|
|
assert array_slice(arr, 2, 1) == ["d"]
|
|
assert array_slice(arr, 0, 2) == ["b", "d"]
|
|
assert array_slice(arr, 0, -1) == ["b", "d"]
|
|
|
|
|
|
def test_slice_negative_offset_counts_from_the_extent():
|
|
arr: list[str | None] = []
|
|
array_set(arr, 1, "b")
|
|
array_set(arr, 9, "j")
|
|
assert array_slice(arr, -1, None) == ["j"]
|
|
# Still negative after the extent is added: nothing, not everything.
|
|
assert array_slice(arr, -20, None) == []
|
|
assert array_slice(make_array(["x", "y", "z"]), -5, None) == []
|
|
assert array_slice(make_array(["x", "y", "z"]), -2, None) == ["y", "z"]
|
|
|
|
|
|
def test_keyed_word():
|
|
assert keyed_word("[a]=1") == ("a", "1")
|
|
assert keyed_word("[two words]=v x") == ("two words", "v x")
|
|
assert keyed_word("plain") is None
|
|
assert keyed_word("[]=x") is None
|
|
assert keyed_word("[k]=") == ("k", "")
|
|
# The split lands on the first `]=`, so a value may hold one.
|
|
assert keyed_word("[a]=x]=y") == ("a", "x]=y")
|
|
|
|
|
|
def test_build_indexed_literal_places_and_continues():
|
|
built = build_indexed_literal(None, ["[3]=x", "y", "[1]=z"], False,
|
|
lambda t: int(t))
|
|
assert built == [None, "z", None, "x", "y"]
|
|
# `+=` starts the cursor at the extent; last index wins.
|
|
appended = build_indexed_literal(["a"], ["b", "[0]=A"], True,
|
|
lambda t: int(t))
|
|
assert appended == ["A", "b"]
|
|
|
|
|
|
def test_build_assoc_literal_modes():
|
|
keyed, errors = build_assoc_literal(None, ["[a]=1", "b", "[a]=2"], False)
|
|
assert keyed == {"a": "2"}
|
|
assert errors == ["b"]
|
|
pairs, errors = build_assoc_literal(None, ["k1", "v1", "[a]=1"], False)
|
|
assert pairs == {"k1": "v1", "[a]=1": ""}
|
|
assert errors == []
|
|
merged, errors = build_assoc_literal({"a": "1"}, ["[b]=2"], True)
|
|
assert merged == {"a": "1", "b": "2"}
|
|
assert errors == []
|
|
replaced, _ = build_assoc_literal({"a": "1"}, ["[b]=2"], False)
|
|
assert replaced == {"b": "2"}
|