Edit autostart, startup order and tags from the info screen
The info screen's Resources table shows Autostart (yes/no), Startup (order, up and down delays) and Tags. o opens a dialog to change them. Only keys that differ are sent, in one `set <kind> config` call through the existing set_net. A confirm lists the old and new value of each key. Tags are removed with `--delete tags` on set config. The CLI also has a `delete ct|vm ... tags` command, but it shares the form that destroys the whole guest, so the app never uses it; nothing in the code builds a delete ct|vm argv. Inputs are validated before sending: startup values must be ints >= 0, and tags must match [A-Za-z0-9_][A-Za-z0-9_+.-]*. They are split on ; , or whitespace and deduplicated. Live test on CT 104: onboot 1 -> 0, startup order=5,up=10, tag tui-test added, then all set back through the dialog. The config ended identical to the baseline. Closes #18 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #30.
This commit is contained in:
1 parent
d97ad22c78
commit
f5f3effce5
6 files changed
+634
No files matched your search
@@ -1388,3 +1388,119 @@ def test_version_argv_and_strips(fake):
|
||||
|
||||
def test_tested_version_constant():
|
||||
assert cli.TESTED_VERSION == "2.4.0"
|
||||
|
||||
|
||||
# --- #18: startup order and tags (spec: "Autostart, startup order and tags") ----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("args", "expected"),
|
||||
[
|
||||
((2, 10, 30), "order=2,up=10,down=30"),
|
||||
((2, 10, None), "order=2,up=10"),
|
||||
((2, None, None), "order=2"),
|
||||
((None, 10, None), "up=10"),
|
||||
((None, None, 30), "down=30"),
|
||||
((None, 10, 30), "up=10,down=30"),
|
||||
((0, 0, 0), "order=0,up=0,down=0"), # 0 is a value, not "unset"
|
||||
((None, None, None), None),
|
||||
],
|
||||
)
|
||||
def test_startup_value_builds_only_the_given_parts(args, expected):
|
||||
assert cli.startup_value(*args) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args", [(-1, None, None), (None, -5, None), (None, None, -30), (1, 2, -1)]
|
||||
)
|
||||
def test_startup_value_rejects_negatives(args):
|
||||
with pytest.raises(ValueError):
|
||||
cli.startup_value(*args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["x", 1.5, [1]])
|
||||
def test_startup_value_rejects_non_ints(bad):
|
||||
for args in ((bad, None, None), (None, bad, None), (None, None, bad)):
|
||||
with pytest.raises(ValueError):
|
||||
cli.startup_value(*args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("order=2,up=10,down=30", (2, 10, 30)),
|
||||
("3", (3, None, None)), # a bare leading number is the order
|
||||
("up=5", (None, 5, None)),
|
||||
("order=2,up=10", (2, 10, None)),
|
||||
("3,up=5", (3, 5, None)),
|
||||
("down=7", (None, None, 7)),
|
||||
(None, (None, None, None)),
|
||||
("", (None, None, None)),
|
||||
],
|
||||
)
|
||||
def test_parse_startup(text, expected):
|
||||
assert cli.parse_startup(text) == expected
|
||||
|
||||
|
||||
@given(
|
||||
st.tuples(*[st.one_of(st.none(), st.integers(min_value=0, max_value=10**6))] * 3)
|
||||
)
|
||||
def test_parse_startup_inverts_startup_value_property(parts):
|
||||
assert cli.parse_startup(cli.startup_value(*parts)) == parts
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("os", "os"),
|
||||
("a;b;c", "a;b;c"),
|
||||
("a,b", "a;b"),
|
||||
("a b", "a;b"), # whitespace separates: two tags, not one with a space
|
||||
("a;b, c d", "a;b;c;d"),
|
||||
("a; b; c", "a;b;c"),
|
||||
("a;b;a;c;b", "a;b;c"), # duplicates dropped, first occurrence wins
|
||||
("c;a;b", "c;a;b"), # order kept, not sorted
|
||||
(";;a;;,, b;", "a;b"), # empties dropped
|
||||
("172.16.41.200", "172.16.41.200"), # IP-like tag is valid
|
||||
("172.16.41.200;community-script;os", "172.16.41.200;community-script;os"),
|
||||
("_x;a+b;a.b;a-b;9", "_x;a+b;a.b;a-b;9"),
|
||||
],
|
||||
)
|
||||
def test_tags_value_normalises(text, expected):
|
||||
assert cli.tags_value(text) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", ["", " ", ";", " ; , ;", "\n\t"])
|
||||
def test_tags_value_empty_is_none(text):
|
||||
assert cli.tags_value(text) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad", ["-x", "a;b!", "!", "a;-b", ".a", "+a", "a/b", "a;b@c", "ä"]
|
||||
)
|
||||
def test_tags_value_rejects_invalid_tags(bad):
|
||||
with pytest.raises(ValueError):
|
||||
cli.tags_value(bad)
|
||||
|
||||
|
||||
_TAG = st.from_regex(r"[A-Za-z0-9_][A-Za-z0-9_+.-]*", fullmatch=True)
|
||||
_SEP = st.sampled_from([";", ",", " ", "; ", ", ", " ", ";;"])
|
||||
|
||||
|
||||
@st.composite
|
||||
def _valid_tag_text(draw):
|
||||
tags = draw(st.lists(_TAG, max_size=6))
|
||||
out = ""
|
||||
for tag in tags:
|
||||
out += tag + draw(_SEP)
|
||||
return out
|
||||
|
||||
|
||||
@given(_valid_tag_text())
|
||||
def test_tags_value_is_idempotent_property(text):
|
||||
once = cli.tags_value(text)
|
||||
if once is None:
|
||||
assert text.replace(";", "").replace(",", "").strip() == ""
|
||||
else:
|
||||
assert cli.tags_value(once) == once
|
||||
assert len(set(once.split(";"))) == len(once.split(";")) # no duplicates
|
||||
Reference in new issue
Block a user