blob: 150469a140ddb22808abbd8aa00c6a785ba13436 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for plan validation, canonicalization, and digests."""
import unittest
from typing import Any
from driver_lab.plans import (
PlanError,
canonical_json,
is_mutating_plan,
plan_digest,
validate_plan,
)
def make_plan(**overrides: object) -> dict[str, Any]:
plan: dict[str, Any] = {
"schema_version": 1,
"run_id": "2026-07-30T00-00-00Z-status-read",
"case_id": "device-status-observation",
"target": {"selector": "lab-target"},
"node": {
"id": "example-device",
"expected_unclaimed": True,
"expected_resource_digest": "sha256:" + "ab" * 32,
},
"access": {"mode": "proxy", "activation": "bind-unclaimed"},
"operations": [
{"kind": "mmio_read32", "resource": "control", "offset": "0x3c"},
],
}
plan.update(overrides)
return plan
class CanonicalizationTest(unittest.TestCase):
def test_digest_is_deterministic(self) -> None:
self.assertEqual(plan_digest(make_plan()), plan_digest(make_plan()))
self.assertTrue(plan_digest(make_plan()).startswith("sha256:"))
def test_numeric_representation_is_normalized(self) -> None:
hex_plan = make_plan()
int_plan = make_plan(
operations=[
{"kind": "mmio_read32", "resource": "control", "offset": 0x3C}
]
)
self.assertEqual(plan_digest(hex_plan), plan_digest(int_plan))
def test_key_order_is_normalized(self) -> None:
reordered = make_plan()
reordered["access"] = {"activation": "bind-unclaimed", "mode": "proxy"}
self.assertEqual(plan_digest(make_plan()), plan_digest(reordered))
def test_every_executable_change_invalidates(self) -> None:
base = plan_digest(make_plan())
changed = [
make_plan(run_id="different-run"),
make_plan(case_id="different-case"),
make_plan(target={"selector": "other-target"}),
make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x40",
}
]
),
make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "status",
"offset": "0x3c",
}
]
),
make_plan(access={"mode": "direct"}),
]
for plan in changed:
self.assertNotEqual(plan_digest(plan), base)
def test_operation_order_is_semantic(self) -> None:
forward = make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x3c",
},
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x40",
},
]
)
reversed_ops = make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x40",
},
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x3c",
},
]
)
self.assertNotEqual(plan_digest(forward), plan_digest(reversed_ops))
def test_canonical_json_is_stable_bytes(self) -> None:
self.assertEqual(
canonical_json(make_plan()), canonical_json(make_plan())
)
class ValidationTest(unittest.TestCase):
def test_unknown_top_level_key_rejected(self) -> None:
plan = make_plan()
plan["driver_url"] = "fuchsia-pkg://evil"
with self.assertRaises(PlanError):
validate_plan(plan)
def test_unknown_operation_key_rejected(self) -> None:
plan = make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x3c",
"moniker": "core/evil",
}
]
)
with self.assertRaises(PlanError):
validate_plan(plan)
def test_takeover_activation_rejected_until_phase_2(self) -> None:
plan = make_plan(access={"mode": "proxy", "activation": "takeover"})
with self.assertRaisesRegex(PlanError, "phase 2"):
validate_plan(plan)
def test_reserved_takeover_keys_rejected_as_reserved_not_unknown(
self,
) -> None:
# The schema carries the phase 2 keys: setting one fails with a
# distinct reserved-key error so phase 2 adoption changes no
# schema shape.
for section, key in (
("node", "expected_bound_driver_url"),
("node", "expected_topology_generation"),
("access", "restoration"),
):
plan = make_plan()
plan[section] = dict(plan[section], **{key: "anything"})
with self.assertRaisesRegex(
PlanError, "reserved until phase 2", msg=key
):
validate_plan(plan)
def test_activation_invalid_in_direct_mode(self) -> None:
plan = make_plan(
access={"mode": "direct", "activation": "bind-unclaimed"}
)
with self.assertRaises(PlanError):
validate_plan(plan)
def test_write_operation_validates(self) -> None:
good = make_plan(
operations=[
{
"kind": "mmio_write32",
"resource": "control",
"offset": "0x3c",
"value": 0x1234_5678,
"write_mask": 0xFFFF_0000,
"precondition": {"expected": 0x0, "mask": 0xFF},
"readback": True,
}
]
)
validated = validate_plan(good)
op = validated["operations"][0]
self.assertEqual(op["kind"], "mmio_write32")
self.assertEqual(op["value"], 0x1234_5678)
self.assertEqual(op["write_mask"], 0xFFFF_0000)
self.assertEqual(op["precondition"], {"expected": 0, "mask": 0xFF})
self.assertTrue(op["readback"])
missing_value = make_plan(
operations=[
{
"kind": "mmio_write32",
"resource": "control",
"offset": "0x3c",
}
]
)
with self.assertRaises(PlanError):
validate_plan(missing_value)
def test_poll_operation_validates(self) -> None:
good = make_plan(
operations=[
{
"kind": "mmio_poll32",
"resource": "control",
"offset": "0x3c",
"expected": 0x1,
"mask": 0x1,
"interval_ns": 1000,
"timeout_ns": 50000,
}
]
)
validated = validate_plan(good)
op = validated["operations"][0]
self.assertEqual(op["kind"], "mmio_poll32")
self.assertEqual(op["expected"], 1)
self.assertEqual(op["mask"], 1)
self.assertEqual(op["interval_ns"], 1000)
self.assertEqual(op["timeout_ns"], 50000)
missing_expected = make_plan(
operations=[
{
"kind": "mmio_poll32",
"resource": "control",
"offset": "0x3c",
"interval_ns": 1000,
"timeout_ns": 50000,
}
]
)
with self.assertRaises(PlanError):
validate_plan(missing_expected)
def test_sequence_operation_validates(self) -> None:
good = make_plan(
operations=[
{
"kind": "sequence",
"items": [
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x3c",
},
{"kind": "delay_ns", "duration_ns": 1000},
{"kind": "barrier", "variant": "memory"},
{
"kind": "mmio_write32",
"resource": "control",
"offset": "0x3c",
"value": 0x42,
},
{
"kind": "mmio_poll32",
"resource": "control",
"offset": "0x3c",
"expected": 0x42,
"interval_ns": 1000,
"timeout_ns": 5000,
},
],
}
]
)
validated = validate_plan(good)
op = validated["operations"][0]
self.assertEqual(op["kind"], "sequence")
self.assertEqual(len(op["items"]), 5)
empty_items = make_plan(operations=[{"kind": "sequence", "items": []}])
with self.assertRaises(PlanError):
validate_plan(empty_items)
too_many_items = make_plan(
operations=[
{
"kind": "sequence",
"items": [
{"kind": "delay_ns", "duration_ns": 100}
for _ in range(65)
],
}
]
)
with self.assertRaises(PlanError):
validate_plan(too_many_items)
def test_empty_operations_rejected(self) -> None:
with self.assertRaises(PlanError):
validate_plan(make_plan(operations=[]))
def test_bad_offset_rejected(self) -> None:
plan = make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "control",
"offset": "0x3c; rm -rf",
}
]
)
with self.assertRaises(PlanError):
validate_plan(plan)
def test_snapshot_operation_validates_items(self) -> None:
good = make_plan(
operations=[
{
"kind": "mmio_snapshot32",
"items": [
{"resource": "control", "offset": "0x3c"},
{"resource": "control", "offset": 0x40},
],
}
]
)
validated = validate_plan(good)
self.assertEqual(validated["operations"][0]["items"][0]["offset"], 0x3C)
bad = make_plan(operations=[{"kind": "mmio_snapshot32", "items": []}])
with self.assertRaises(PlanError):
validate_plan(bad)
def test_fidl_call_operation_validates(self) -> None:
good = make_plan(
access={"mode": "direct"},
operations=[
{
"kind": "fidl_call",
"method": "GetStatus",
"args": {"flags": 1},
}
],
)
validated = validate_plan(good)
self.assertEqual(validated["operations"][0]["kind"], "fidl_call")
self.assertEqual(validated["operations"][0]["method"], "GetStatus")
self.assertEqual(validated["operations"][0]["args"], {"flags": 1})
bad_method = make_plan(operations=[{"kind": "fidl_call", "method": ""}])
with self.assertRaises(PlanError):
validate_plan(bad_method)
bad_args = make_plan(
operations=[
{"kind": "fidl_call", "method": "Foo", "args": "not-a-map"}
]
)
with self.assertRaises(PlanError):
validate_plan(bad_args)
def test_state_knob_trigger_operations_validate(self) -> None:
good = make_plan(
operations=[
{"kind": "state_read32", "offset": "0x00"},
{
"kind": "state_poll32",
"offset": "0x04",
"expected": 0,
"mask": 0xFFFF_FFFF,
},
{
"kind": "knob_write32",
"offset": "0x08",
"value": 5000,
},
{
"kind": "trigger_write32",
"offset": "0x10",
"arg": 3,
},
{
"kind": "trigger_write32",
"resource": "custom_state",
"offset": "0x14",
},
{
"kind": "sequence",
"items": [
{
"kind": "knob_write32",
"offset": "0x08",
"value": 1000,
},
{"kind": "trigger_write32", "offset": "0x10", "arg": 1},
{"kind": "state_read32", "offset": "0x04"},
{
"kind": "state_poll32",
"offset": "0x04",
"expected": 0,
},
],
},
]
)
validated = validate_plan(good)
ops = validated["operations"]
self.assertEqual(ops[0]["kind"], "state_read32")
self.assertEqual(ops[0]["resource"], "state0")
self.assertEqual(ops[0]["offset"], 0x00)
self.assertEqual(ops[1]["kind"], "state_poll32")
self.assertEqual(ops[1]["resource"], "state0")
self.assertEqual(ops[1]["offset"], 0x04)
self.assertEqual(ops[2]["kind"], "knob_write32")
self.assertEqual(ops[2]["resource"], "state0")
self.assertEqual(ops[2]["value"], 5000)
self.assertEqual(ops[3]["kind"], "trigger_write32")
self.assertEqual(ops[3]["resource"], "state0")
self.assertEqual(ops[3]["value"], 3)
self.assertFalse(ops[3]["readback"])
self.assertEqual(ops[4]["kind"], "trigger_write32")
self.assertEqual(ops[4]["resource"], "custom_state")
self.assertEqual(ops[4]["value"], 1)
seq_items = ops[5]["items"]
self.assertEqual(seq_items[0]["kind"], "knob_write32")
self.assertEqual(seq_items[0]["resource"], "state0")
self.assertEqual(seq_items[1]["kind"], "trigger_write32")
self.assertEqual(seq_items[1]["resource"], "state0")
self.assertEqual(seq_items[1]["value"], 1)
self.assertEqual(seq_items[2]["kind"], "state_read32")
self.assertEqual(seq_items[2]["resource"], "state0")
self.assertEqual(seq_items[3]["kind"], "state_poll32")
self.assertEqual(seq_items[3]["resource"], "state0")
def test_state_knob_trigger_mutation_detection(self) -> None:
ro_plan = validate_plan(
make_plan(
operations=[
{"kind": "state_read32", "offset": "0x00"},
{
"kind": "state_poll32",
"offset": "0x04",
"expected": 0,
},
]
)
)
self.assertFalse(is_mutating_plan(ro_plan))
knob_plan = validate_plan(
make_plan(
operations=[
{"kind": "knob_write32", "offset": "0x08", "value": 1}
]
)
)
self.assertTrue(is_mutating_plan(knob_plan))
trigger_plan = validate_plan(
make_plan(
operations=[
{"kind": "trigger_write32", "offset": "0x10", "arg": 1}
]
)
)
self.assertTrue(is_mutating_plan(trigger_plan))
seq_trigger_plan = validate_plan(
make_plan(
operations=[
{
"kind": "sequence",
"items": [
{"kind": "trigger_write32", "offset": "0x10"}
],
}
]
)
)
self.assertTrue(is_mutating_plan(seq_trigger_plan))
def test_mismatched_mmio_and_state_resources_rejected(self) -> None:
with self.assertRaisesRegex(PlanError, "StateBank resource"):
validate_plan(
make_plan(
operations=[
{
"kind": "mmio_read32",
"resource": "state0",
"offset": "0x00",
}
]
)
)
with self.assertRaisesRegex(PlanError, "StateBank resource"):
validate_plan(
make_plan(
operations=[
{
"kind": "mmio_write32",
"resource": "state0",
"offset": "0x08",
"value": 1,
}
]
)
)
with self.assertRaisesRegex(PlanError, "MMIO resource"):
validate_plan(
make_plan(
operations=[
{
"kind": "state_read32",
"resource": "mmio0",
"offset": "0x00",
}
]
)
)
with self.assertRaisesRegex(PlanError, "MMIO resource"):
validate_plan(
make_plan(
operations=[
{
"kind": "knob_write32",
"resource": "mmio0",
"offset": "0x08",
"value": 1,
}
]
)
)
if __name__ == "__main__":
unittest.main()