diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c index eacfb6133..26330b14b 100644 --- a/src/modules/module-avb/acmp.c +++ b/src/modules/module-avb/acmp.c @@ -82,7 +82,7 @@ static void pending_free(struct acmp *acmp, struct pending *p) static void pending_destroy(struct acmp *acmp) { struct pending *p, *t; - for (uint32_t list_id = 0; list_id < PENDING_CONTROLLER; list_id++) { + for (uint32_t list_id = 0; list_id <= PENDING_CONTROLLER; list_id++) { spa_list_for_each_safe(p, t, &acmp->pending[list_id], link) { pending_free(acmp, p); } @@ -174,13 +174,14 @@ static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void return 0; memcpy(buf, m, len); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); + stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); if (stream == NULL) { status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; goto done; } - AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); reply->stream_id = htobe64(stream->id); stream_activate(stream, ntohs(reply->talker_unique_id), now); @@ -251,14 +252,14 @@ static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const v return 0; memcpy(buf, m, len); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); + stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); if (stream == NULL) { status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; goto done; } - AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); - stream_deactivate(stream, now); done: diff --git a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c index 0149d633b..73b7d2548 100644 --- a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c +++ b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c @@ -99,7 +99,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void desc = server_find_descriptor(server, desc_type, desc_id); if (desc == NULL) - return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len); + return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len); entity_state = desc->ptr; lock = &entity_state->state.lock_state; @@ -148,7 +148,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void // If the lock is taken again by device if (ctrler_id == lock->locked_id) { lock->base_info.expire_timeout += - AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND; + AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC; lock->is_locked = true; } else { diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c index fccf6b178..e655f02c5 100644 --- a/src/modules/module-avb/aecp-aem.c +++ b/src/modules/module-avb/aecp-aem.c @@ -27,7 +27,8 @@ static int handle_acquire_entity_avb_legacy(struct aecp *aecp, int64_t now, const void *m, int len) { struct server *server = aecp->server; - const struct avb_packet_aecp_aem *p = m; + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); const struct avb_packet_aecp_aem_acquire *ae; const struct descriptor *desc; uint16_t desc_type, desc_id; @@ -53,7 +54,8 @@ static int handle_lock_entity_avb_legacy(struct aecp *aecp, int64_t now, const void *m, int len) { struct server *server = aecp->server; - const struct avb_packet_aecp_aem *p = m; + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); const struct avb_packet_aecp_aem_acquire *ae; const struct descriptor *desc; uint16_t desc_type, desc_id; diff --git a/src/modules/module-avb/avb-transport-loopback.h b/src/modules/module-avb/avb-transport-loopback.h new file mode 100644 index 000000000..d783ad55c --- /dev/null +++ b/src/modules/module-avb/avb-transport-loopback.h @@ -0,0 +1,149 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_TRANSPORT_LOOPBACK_H +#define AVB_TRANSPORT_LOOPBACK_H + +#include +#include +#include +#include +#include + +#include "internal.h" +#include "packets.h" + +#define AVB_LOOPBACK_MAX_PACKETS 64 +#define AVB_LOOPBACK_MAX_PACKET_SIZE 2048 + +struct avb_loopback_packet { + uint8_t dest[6]; + uint16_t type; + size_t size; + uint8_t data[AVB_LOOPBACK_MAX_PACKET_SIZE]; +}; + +struct avb_loopback_transport { + struct avb_loopback_packet packets[AVB_LOOPBACK_MAX_PACKETS]; + int packet_count; + int packet_read; +}; + +static inline int avb_loopback_setup(struct server *server) +{ + struct avb_loopback_transport *t; + static const uint8_t test_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + + t = calloc(1, sizeof(*t)); + if (t == NULL) + return -errno; + + server->transport_data = t; + + memcpy(server->mac_addr, test_mac, 6); + server->ifindex = 1; + server->entity_id = (uint64_t)server->mac_addr[0] << 56 | + (uint64_t)server->mac_addr[1] << 48 | + (uint64_t)server->mac_addr[2] << 40 | + (uint64_t)0xff << 32 | + (uint64_t)0xfe << 24 | + (uint64_t)server->mac_addr[3] << 16 | + (uint64_t)server->mac_addr[4] << 8 | + (uint64_t)server->mac_addr[5]; + + return 0; +} + +static inline int avb_loopback_send_packet(struct server *server, + const uint8_t dest[6], uint16_t type, void *data, size_t size) +{ + struct avb_loopback_transport *t = server->transport_data; + struct avb_loopback_packet *pkt; + struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data; + + if (t->packet_count >= AVB_LOOPBACK_MAX_PACKETS) + return -ENOSPC; + if (size > AVB_LOOPBACK_MAX_PACKET_SIZE) + return -EMSGSIZE; + + /* Fill in the ethernet header like the raw transport does */ + memcpy(hdr->dest, dest, 6); + memcpy(hdr->src, server->mac_addr, 6); + hdr->type = htons(type); + + pkt = &t->packets[t->packet_count % AVB_LOOPBACK_MAX_PACKETS]; + memcpy(pkt->dest, dest, 6); + pkt->type = type; + pkt->size = size; + memcpy(pkt->data, data, size); + t->packet_count++; + + return 0; +} + +/** + * Return a dummy fd for protocol handlers that create their own sockets. + * Uses eventfd so pw_loop_add_io() has a valid fd to work with. + */ +static inline int avb_loopback_make_socket(struct server *server, + uint16_t type, const uint8_t mac[6]) +{ + int fd; + + fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd < 0) + return -errno; + + return fd; +} + +static inline void avb_loopback_destroy(struct server *server) +{ + free(server->transport_data); + server->transport_data = NULL; +} + +static const struct avb_transport_ops avb_transport_loopback = { + .setup = avb_loopback_setup, + .send_packet = avb_loopback_send_packet, + .make_socket = avb_loopback_make_socket, + .destroy = avb_loopback_destroy, +}; + +/** Get the number of captured sent packets */ +static inline int avb_loopback_get_packet_count(struct server *server) +{ + struct avb_loopback_transport *t = server->transport_data; + return t->packet_count - t->packet_read; +} + +/** Read the next captured sent packet, returns packet size or -1 */ +static inline int avb_loopback_get_packet(struct server *server, + void *buf, size_t bufsize) +{ + struct avb_loopback_transport *t = server->transport_data; + struct avb_loopback_packet *pkt; + + if (t->packet_read >= t->packet_count) + return -1; + + pkt = &t->packets[t->packet_read % AVB_LOOPBACK_MAX_PACKETS]; + t->packet_read++; + + if (pkt->size > bufsize) + return -1; + + memcpy(buf, pkt->data, pkt->size); + return pkt->size; +} + +/** Clear all captured packets */ +static inline void avb_loopback_clear_packets(struct server *server) +{ + struct avb_loopback_transport *t = server->transport_data; + t->packet_count = 0; + t->packet_read = 0; +} + +#endif /* AVB_TRANSPORT_LOOPBACK_H */ diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c index 7cd7e8e7a..b3a40133d 100644 --- a/src/modules/module-avb/avdecc.c +++ b/src/modules/module-avb/avdecc.c @@ -84,7 +84,7 @@ static void on_socket_data(void *data, int fd, uint32_t mask) } } -int avb_server_send_packet(struct server *server, const uint8_t dest[6], +static int raw_send_packet(struct server *server, const uint8_t dest[6], uint16_t type, void *data, size_t size) { struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data; @@ -101,6 +101,12 @@ int avb_server_send_packet(struct server *server, const uint8_t dest[6], return res; } +int avb_server_send_packet(struct server *server, const uint8_t dest[6], + uint16_t type, void *data, size_t size) +{ + return server->transport->send_packet(server, dest, type, data, size); +} + static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6]) { struct sock_fprog filter; @@ -136,7 +142,7 @@ static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_ return 0; } -int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) +static int raw_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) { int fd, res; struct ifreq req; @@ -209,13 +215,20 @@ error_close: return res; } -static int setup_socket(struct server *server) +int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) +{ + if (server->transport && server->transport->make_socket) + return server->transport->make_socket(server, type, mac); + return raw_make_socket(server, type, mac); +} + +static int raw_transport_setup(struct server *server) { struct impl *impl = server->impl; int fd, res; static const uint8_t bmac[6] = AVB_BROADCAST_MAC; - fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac); + fd = raw_make_socket(server, AVB_TSN_ETH, bmac); if (fd < 0) return fd; @@ -244,6 +257,21 @@ error_no_source: return res; } +static void raw_transport_destroy(struct server *server) +{ + struct impl *impl = server->impl; + if (server->source) + pw_loop_destroy_source(impl->loop, server->source); + server->source = NULL; +} + +const struct avb_transport_ops avb_transport_raw = { + .setup = raw_transport_setup, + .send_packet = raw_send_packet, + .make_socket = raw_make_socket, + .destroy = raw_transport_destroy, +}; + struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props) { struct server *server; @@ -269,7 +297,10 @@ struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props) server->debug_messages = false; - if ((res = setup_socket(server)) < 0) + if (server->transport == NULL) + server->transport = &avb_transport_raw; + + if ((res = server->transport->setup(server)) < 0) goto error_free; @@ -315,12 +346,10 @@ void avdecc_server_add_listener(struct server *server, struct spa_hook *listener void avdecc_server_free(struct server *server) { - struct impl *impl = server->impl; - server_destroy_descriptors(server); spa_list_remove(&server->link); - if (server->source) - pw_loop_destroy_source(impl->loop, server->source); + if (server->transport) + server->transport->destroy(server); pw_timer_queue_cancel(&server->timer); spa_hook_list_clean(&server->listener_list); free(server->ifname); diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h index 82ced2f21..f3ddb172e 100644 --- a/src/modules/module-avb/internal.h +++ b/src/modules/module-avb/internal.h @@ -17,6 +17,15 @@ struct avb_mrp; #define AVB_TSN_ETH 0x22f0 #define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }; +struct avb_transport_ops { + int (*setup)(struct server *server); + int (*send_packet)(struct server *server, const uint8_t dest[6], + uint16_t type, void *data, size_t size); + int (*make_socket)(struct server *server, uint16_t type, + const uint8_t mac[6]); + void (*destroy)(struct server *server); +}; + struct impl { struct pw_loop *loop; struct pw_timer_queue *timer_queue; @@ -77,6 +86,9 @@ struct server { uint64_t entity_id; int ifindex; + const struct avb_transport_ops *transport; + void *transport_data; + struct spa_source *source; struct pw_timer timer; @@ -102,7 +114,6 @@ static inline void server_destroy_descriptors(struct server *server) struct descriptor *d, *t; spa_list_for_each_safe(d, t, &server->descriptors, link) { - free(d->ptr); spa_list_remove(&d->link); free(d); } @@ -145,6 +156,8 @@ void avdecc_server_free(struct server *server); void avdecc_server_add_listener(struct server *server, struct spa_hook *listener, const struct server_events *events, void *data); +extern const struct avb_transport_ops avb_transport_raw; + int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]); int avb_server_send_packet(struct server *server, const uint8_t dest[6], diff --git a/src/modules/module-avb/mrp.c b/src/modules/module-avb/mrp.c index 73d5275ca..c6505d41b 100644 --- a/src/modules/module-avb/mrp.c +++ b/src/modules/module-avb/mrp.c @@ -302,6 +302,8 @@ const char *avb_mrp_notify_name(uint8_t notify) const char *avb_mrp_send_name(uint8_t send) { switch(send) { + case 0: + return "none"; case AVB_MRP_SEND_NEW: return "new"; case AVB_MRP_SEND_JOININ: diff --git a/src/modules/module-avb/mrp.h b/src/modules/module-avb/mrp.h index 78683412c..399343267 100644 --- a/src/modules/module-avb/mrp.h +++ b/src/modules/module-avb/mrp.h @@ -88,13 +88,13 @@ struct avb_packet_mrp_footer { #define AVB_MRP_ATTRIBUTE_EVENT_LV 5 #define AVB_MRP_ATTRIBUTE_EVENT_LVA 6 -#define AVB_MRP_SEND_NEW 0 -#define AVB_MRP_SEND_JOININ 1 -#define AVB_MRP_SEND_IN 2 -#define AVB_MRP_SEND_JOINMT 3 -#define AVB_MRP_SEND_MT 4 -#define AVB_MRP_SEND_LV 5 -#define AVB_MRP_SEND_LVA 6 +#define AVB_MRP_SEND_NEW 1 +#define AVB_MRP_SEND_JOININ 2 +#define AVB_MRP_SEND_IN 3 +#define AVB_MRP_SEND_JOINMT 4 +#define AVB_MRP_SEND_MT 5 +#define AVB_MRP_SEND_LV 6 +#define AVB_MRP_SEND_LVA 7 #define AVB_MRP_NOTIFY_NEW 1 #define AVB_MRP_NOTIFY_JOIN 2 diff --git a/src/modules/module-avb/msrp.c b/src/modules/module-avb/msrp.c index 92d1e65b4..611ddd537 100644 --- a/src/modules/module-avb/msrp.c +++ b/src/modules/module-avb/msrp.c @@ -91,7 +91,7 @@ static int encode_talker(struct msrp *msrp, struct attr *a, void *m) *t = a->attr.attr.talker; ev = SPA_PTROFF(t, sizeof(*t), uint8_t); - *ev = a->attr.mrp->pending_send * 6 * 6; + *ev = (a->attr.mrp->pending_send - 1) * 6 * 6; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -170,7 +170,7 @@ static int encode_listener(struct msrp *msrp, struct attr *a, void *m) *l = a->attr.attr.listener; ev = SPA_PTROFF(l, sizeof(*l), uint8_t); - *ev = a->attr.mrp->pending_send * 6 * 6; + *ev = (a->attr.mrp->pending_send - 1) * 6 * 6; ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t); *ev = a->attr.param * 4 * 4 * 4; @@ -226,7 +226,7 @@ static int encode_domain(struct msrp *msrp, struct attr *a, void *m) *d = a->attr.attr.domain; ev = SPA_PTROFF(d, sizeof(*d), uint8_t); - *ev = a->attr.mrp->pending_send * 36; + *ev = (a->attr.mrp->pending_send - 1) * 36; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -332,7 +332,8 @@ static void msrp_notify(void *data, uint64_t now, uint8_t notify) { struct attr *a = data; struct msrp *msrp = a->msrp; - return dispatch[a->attr.type].notify(msrp, now, a, notify); + if (dispatch[a->attr.type].notify) + dispatch[a->attr.type].notify(msrp, now, a, notify); } static const struct avb_mrp_attribute_events mrp_attr_events = { diff --git a/src/modules/module-avb/mvrp.c b/src/modules/module-avb/mvrp.c index 20862c2ae..e2e501a9e 100644 --- a/src/modules/module-avb/mvrp.c +++ b/src/modules/module-avb/mvrp.c @@ -84,7 +84,7 @@ static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m) *d = a->attr.attr.vid; ev = SPA_PTROFF(d, sizeof(*d), uint8_t); - *ev = a->attr.mrp->pending_send * 36; + *ev = (a->attr.mrp->pending_send - 1) * 36; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -171,7 +171,8 @@ static void mvrp_notify(void *data, uint64_t now, uint8_t notify) { struct attr *a = data; struct mvrp *mvrp = a->mvrp; - return dispatch[a->attr.type].notify(mvrp, now, a, notify); + if (dispatch[a->attr.type].notify) + dispatch[a->attr.type].notify(mvrp, now, a, notify); } static const struct avb_mrp_attribute_events mrp_attr_events = { diff --git a/test/avb-bugs.md b/test/avb-bugs.md new file mode 100644 index 000000000..eaafe4c6c --- /dev/null +++ b/test/avb-bugs.md @@ -0,0 +1,143 @@ +# AVB Module Bugs Found via Test Suite + +The following bugs were discovered by building a software test harness +for the AVB protocol stack. All have been fixed in the accompanying +patch series. + +## 1. Heap corruption in server_destroy_descriptors + +**File:** `src/modules/module-avb/internal.h` +**Commit:** `69c721006` + +`server_destroy_descriptors()` called `free(d->ptr)` followed by +`free(d)`, but `d->ptr` points into the same allocation as `d` +(set via `SPA_PTROFF(d, sizeof(struct descriptor), void)` in +`server_add_descriptor()`). This is a double-free / heap corruption +that could cause crashes or memory corruption when tearing down an +AVB server. + +**Fix:** Remove the erroneous `free(d->ptr)` call. + +## 2. NULL pointer dereference in MSRP notify dispatch + +**File:** `src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c` +**Commit:** `b056e9f85` + +`msrp_notify()` unconditionally calls `dispatch[a->attr.type].notify()` +but the dispatch table entry for `AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED` +has `notify = NULL`. If a talker-failed attribute receives a registrar +state change (e.g., `RX_NEW` triggers `NOTIFY_NEW`), this crashes with +a NULL pointer dereference. The same unguarded pattern exists in +`mvrp_notify()`. + +**Fix:** Add `if (dispatch[a->attr.type].notify)` NULL check before +calling, matching the defensive pattern already used in the encode path. + +## 3. MRP NEW messages never transmitted + +**File:** `src/modules/module-avb/mrp.h`, `src/modules/module-avb/mrp.c`, +`src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c` +**Commit:** `bc2c41daa` + +`AVB_MRP_SEND_NEW` was defined as `0`. The MSRP and MVRP event handlers +skip attributes with `if (!a->attr.mrp->pending_send)`, treating `0` as +"no pending send". Since the MRP state machine sets `pending_send` to +`AVB_MRP_SEND_NEW` (0) when an attribute in state VN or AN receives a +TX event, NEW messages were silently dropped instead of being +transmitted. This violates IEEE 802.1Q which requires NEW messages to +be sent when an attribute is first declared. + +In practice, the attribute would cycle through VN -> AN -> AA over +successive TX events, eventually sending a JOINMT instead of the +initial NEW. The protocol still functioned because JOINMT also +registers the attribute, but the initial declaration was lost. + +**Fix:** Shift all `AVB_MRP_SEND_*` values to start at 1, so that 0 +unambiguously means "no send pending". Update MSRP and MVRP encoders +to subtract 1 when encoding to the IEEE 802.1Q wire format. + +## 4. ACMP error responses sent with wrong message type + +**File:** `src/modules/module-avb/acmp.c` +**Commit:** `9f4147104` + +In `handle_connect_tx_command()` and `handle_disconnect_tx_command()`, +`AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` is called after the `goto done` +jump target. When `find_stream()` fails (returns NULL), the code jumps +to `done:` without setting the message type, so the error response is +sent with the original command message type (e.g., +`CONNECT_TX_COMMAND = 0`) instead of the correct response type +(`CONNECT_TX_RESPONSE = 1`). + +A controller receiving this malformed response would not recognize it +as a response to its command and would eventually time out. + +**Fix:** Move `AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` before the +`find_stream()` call so the response type is always set correctly. + +## 5. ACMP pending_destroy skips controller cleanup + +**File:** `src/modules/module-avb/acmp.c` +**Commit:** `9f4147104` + +`pending_destroy()` iterates with `list_id < PENDING_CONTROLLER` +(where `PENDING_CONTROLLER = 2`), which only cleans up +`PENDING_TALKER` (0) and `PENDING_LISTENER` (1) lists, skipping +`PENDING_CONTROLLER` (2). Any pending controller requests leak on +shutdown. + +**Fix:** Change `<` to `<=` to include the controller list. + +## 6. Legacy AECP handlers read payload at wrong offset + +**File:** `src/modules/module-avb/aecp-aem.c` + +`handle_acquire_entity_avb_legacy()` and `handle_lock_entity_avb_legacy()` +assign `const struct avb_packet_aecp_aem *p = m;` where `m` is the full +ethernet frame (starting with `struct avb_ethernet_header`). The handlers +then access `p->payload` to read the acquire/lock fields, but this reads +from `m + offsetof(avb_packet_aecp_aem, payload)` instead of the correct +`m + sizeof(avb_ethernet_header) + offsetof(avb_packet_aecp_aem, payload)`. +This causes `descriptor_type` and `descriptor_id` to be read from the +wrong position, leading to incorrect descriptor lookups. + +All other AEM command handlers (e.g., `handle_read_descriptor_common`) +correctly derive `p` via `SPA_PTROFF(h, sizeof(*h), void)`. + +**Fix:** Change `const struct avb_packet_aecp_aem *p = m;` to properly +skip the ethernet header: +```c +const struct avb_ethernet_header *h = m; +const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); +``` + +## 7. Milan LOCK_ENTITY error response uses wrong packet pointer + +**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c` + +In `handle_cmd_lock_entity_milan_v12()`, when `server_find_descriptor()` +returns NULL, `reply_status()` is called with `p` (the AEM packet pointer +past the ethernet header) instead of `m` (the full ethernet frame). +`reply_status()` assumes its third argument is the full frame and casts +it as `struct avb_ethernet_header *`. With the wrong pointer, the +response ethernet header (including destination MAC) is corrupted. + +**Fix:** Change `reply_status(aecp, ..., p, len)` to +`reply_status(aecp, ..., m, len)`. + +## 8. Lock entity re-lock timeout uses wrong units + +**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c` + +When a controller that already holds the lock sends another lock request +(to refresh it), the expire timeout is extended by: +```c +lock->base_info.expire_timeout += + AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND; +``` +`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND` is `60` (raw seconds), +but `expire_timeout` is in nanoseconds. This adds only 60 nanoseconds +instead of 60 seconds. The initial lock correctly uses +`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC`. + +**Fix:** Multiply by `SPA_NSEC_PER_SEC` to match the nanosecond units. diff --git a/test/meson.build b/test/meson.build index 5e38db383..55443bad3 100644 --- a/test/meson.build +++ b/test/meson.build @@ -163,3 +163,42 @@ if valgrind.found() env : valgrind_env, timeout_multiplier : 3) endif + +if build_module_avb + avb_test_inc = [pwtest_inc, include_directories('../src/modules')] + avb_module_sources = [ + '../src/modules/module-avb/avb.c', + '../src/modules/module-avb/adp.c', + '../src/modules/module-avb/acmp.c', + '../src/modules/module-avb/aecp.c', + '../src/modules/module-avb/aecp-aem.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-available.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c', + '../src/modules/module-avb/es-builder.c', + '../src/modules/module-avb/avdecc.c', + '../src/modules/module-avb/descriptors.c', + '../src/modules/module-avb/maap.c', + '../src/modules/module-avb/mmrp.c', + '../src/modules/module-avb/mrp.c', + '../src/modules/module-avb/msrp.c', + '../src/modules/module-avb/mvrp.c', + '../src/modules/module-avb/srp.c', + '../src/modules/module-avb/stream.c', + ] + test('test-avb', + executable('test-avb', + ['test-avb.c'] + avb_module_sources, + include_directories: avb_test_inc, + dependencies: [spa_dep, pipewire_dep, mathlib, dl_lib, rt_lib], + link_with: pwtest_lib) + ) +endif diff --git a/test/test-avb-utils.h b/test/test-avb-utils.h new file mode 100644 index 000000000..d4552f985 --- /dev/null +++ b/test/test-avb-utils.h @@ -0,0 +1,356 @@ +/* AVB test utilities */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef TEST_AVB_UTILS_H +#define TEST_AVB_UTILS_H + +#include + +#include "module-avb/internal.h" +#include "module-avb/packets.h" +#include "module-avb/adp.h" +#include "module-avb/acmp.h" +#include "module-avb/mrp.h" +#include "module-avb/msrp.h" +#include "module-avb/mvrp.h" +#include "module-avb/mmrp.h" +#include "module-avb/maap.h" +#include "module-avb/aecp.h" +#include "module-avb/aecp-aem.h" +#include "module-avb/aecp-aem-descriptors.h" +#include "module-avb/aecp-aem-state.h" +#include "module-avb/iec61883.h" +#include "module-avb/aaf.h" +#include "module-avb/stream.h" +#include "module-avb/descriptors.h" +#include "module-avb/avb-transport-loopback.h" + +#define server_emit_message(s,n,m,l) \ + spa_hook_list_call(&(s)->listener_list, struct server_events, message, 0, n, m, l) +#define server_emit_periodic(s,n) \ + spa_hook_list_call(&(s)->listener_list, struct server_events, periodic, 0, n) + +/** + * Create a test AVB server with loopback transport. + * All protocol handlers are registered. No network access required. + */ +static inline struct server *avb_test_server_new(struct impl *impl) +{ + struct server *server; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_LEGACY; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add a minimal entity descriptor so ADP can advertise. + * We skip init_descriptors() because it creates streams that + * need a pw_core connection. */ + { + struct avb_aem_desc_entity entity; + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(server->entity_id); + entity.entity_model_id = htobe64(0x0001000000000001ULL); + entity.entity_capabilities = htonl( + AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED); + entity.talker_stream_sources = htons(1); + entity.talker_capabilities = htons( + AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED | + AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE); + entity.listener_stream_sinks = htons(1); + entity.listener_capabilities = htons( + AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED | + AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK); + entity.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + } + + return server; + +error: + free(server->ifname); + free(server); + return NULL; +} + +static inline void avb_test_server_free(struct server *server) +{ + avdecc_server_free(server); +} + +/** + * Inject a raw packet into the server's protocol dispatch. + * This simulates receiving a packet from the network. + */ +static inline void avb_test_inject_packet(struct server *server, + uint64_t now, const void *data, int len) +{ + server_emit_message(server, now, data, len); +} + +/** + * Trigger the periodic callback with a given timestamp. + * Use this to advance time and test timeout/readvertise logic. + */ +static inline void avb_test_tick(struct server *server, uint64_t now) +{ + server_emit_periodic(server, now); +} + +/** + * Build an ADP entity available packet. + * Returns the packet size, or -1 on error. + */ +static inline int avb_test_build_adp_entity_available( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id, + int valid_time) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE); + AVB_PACKET_ADP_SET_VALID_TIME(p, valid_time); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an ADP entity departing packet. + */ +static inline int avb_test_build_adp_entity_departing( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an ADP entity discover packet. + */ +static inline int avb_test_build_adp_entity_discover( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an AECP AEM command packet for injection. + * Returns packet size, or -1 on error. + */ +static inline int avb_test_build_aecp_aem( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t target_guid, + uint64_t controller_guid, + uint16_t sequence_id, + uint16_t command_type, + const void *payload, size_t payload_size) +{ + struct avb_ethernet_header *h; + struct avb_packet_aecp_aem *p; + size_t len = sizeof(*h) + sizeof(*p) + payload_size; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = SPA_PTROFF(h, sizeof(*h), void); + AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_AECP); + AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, AVB_AECP_MESSAGE_TYPE_AEM_COMMAND); + AVB_PACKET_AECP_SET_STATUS(&p->aecp, 0); + AVB_PACKET_SET_LENGTH(&p->aecp.hdr, payload_size + 12); + p->aecp.target_guid = htobe64(target_guid); + p->aecp.controller_guid = htobe64(controller_guid); + p->aecp.sequence_id = htons(sequence_id); + AVB_PACKET_AEM_SET_COMMAND_TYPE(p, command_type); + + if (payload && payload_size > 0) + memcpy(p->payload, payload, payload_size); + + return len; +} + +/** + * Create a test AVB server in Milan v1.2 mode with loopback transport. + * The entity descriptor is properly sized for Milan state (lock, unsol). + */ +static inline struct server *avb_test_server_new_milan(struct impl *impl) +{ + struct server *server; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_MILAN_V12; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add Milan-sized entity descriptor with lock/unsol state */ + { + struct aecp_aem_entity_milan_state entity_state; + memset(&entity_state, 0, sizeof(entity_state)); + entity_state.state.desc.entity_id = htobe64(server->entity_id); + entity_state.state.desc.entity_model_id = htobe64(0x0001000000000001ULL); + entity_state.state.desc.entity_capabilities = htonl( + AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED); + entity_state.state.desc.talker_stream_sources = htons(1); + entity_state.state.desc.talker_capabilities = htons( + AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED | + AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE); + entity_state.state.desc.listener_stream_sinks = htons(1); + entity_state.state.desc.listener_capabilities = htons( + AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED | + AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK); + entity_state.state.desc.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity_state), &entity_state); + } + + return server; + +error: + free(server->ifname); + free(server); + return NULL; +} + +#endif /* TEST_AVB_UTILS_H */ diff --git a/test/test-avb.c b/test/test-avb.c new file mode 100644 index 000000000..11ad0a754 --- /dev/null +++ b/test/test-avb.c @@ -0,0 +1,4017 @@ +/* AVB tests */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#include "pwtest.h" + +#include + +#include "module-avb/aecp-aem-descriptors.h" +#include "module-avb/aecp-aem.h" +#include "module-avb/aecp-aem-types.h" +#include "module-avb/aecp-aem-cmds-resps/cmd-lock-entity.h" +#include "test-avb-utils.h" + +static struct impl *test_impl_new(void) +{ + struct impl *impl; + struct pw_main_loop *ml; + struct pw_context *context; + + pw_init(0, NULL); + + ml = pw_main_loop_new(NULL); + pwtest_ptr_notnull(ml); + + context = pw_context_new(pw_main_loop_get_loop(ml), + pw_properties_new( + PW_KEY_CONFIG_NAME, "null", + NULL), 0); + pwtest_ptr_notnull(context); + + impl = calloc(1, sizeof(*impl)); + pwtest_ptr_notnull(impl); + + impl->loop = pw_main_loop_get_loop(ml); + impl->timer_queue = pw_context_get_timer_queue(context); + impl->context = context; + spa_list_init(&impl->servers); + + return impl; +} + +static void test_impl_free(struct impl *impl) +{ + struct server *s; + spa_list_consume(s, &impl->servers, link) + avb_test_server_free(s); + free(impl); + pw_deinit(); +} + +/* + * Test: inject an ADP ENTITY_AVAILABLE packet and verify + * that the server processes it without error. + */ +PWTEST(avb_adp_entity_available) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; + uint64_t remote_entity_id = 0x020000fffe000002ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build and inject an entity available packet from a remote device */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + pwtest_int_gt(len, 0); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* The packet should have been processed without crashing. + * We can't easily inspect ADP internal state without exposing it, + * but we can verify the server is still functional by doing another + * inject and triggering periodic. */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: inject ENTITY_AVAILABLE then ENTITY_DEPARTING for the same entity. + */ +PWTEST(avb_adp_entity_departing) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x03 }; + uint64_t remote_entity_id = 0x020000fffe000003ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* First make the entity known */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Now send departing */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: inject ENTITY_DISCOVER with entity_id=0 (discover all). + * The server should respond with its own entity advertisement + * once it has one (after periodic runs check_advertise). + */ +PWTEST(avb_adp_entity_discover) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x04 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Trigger periodic to let the server advertise its own entity + * (check_advertise reads the entity descriptor) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Send discover-all (entity_id = 0) */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, 0); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* The server should have sent an advertise response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: entity timeout — add an entity, then advance time past + * valid_time + 2 seconds and verify periodic cleans it up. + */ +PWTEST(avb_adp_entity_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x05 }; + uint64_t remote_entity_id = 0x020000fffe000005ULL; + int valid_time = 10; /* seconds */ + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add entity */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, valid_time); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Tick at various times before timeout — entity should survive */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 10 * SPA_NSEC_PER_SEC); + + /* Tick past valid_time + 2 seconds from last_time (1s + 12s = 13s) */ + avb_test_tick(server, 14 * SPA_NSEC_PER_SEC); + + /* The entity should have been timed out and cleaned up. + * If the entity was still present and had advertise=true, a departing + * packet would be sent. Inject a discover to verify: if the entity + * is gone, no response for that specific entity_id. */ + + avb_loopback_clear_packets(server); + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 15 * SPA_NSEC_PER_SEC, pkt, len); + + /* Remote entities don't have advertise=true, so even before timeout + * a discover for them wouldn't generate a response. But at least + * the timeout path was exercised without crashes. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: basic MRP attribute lifecycle — create, begin, join. + */ +PWTEST(avb_mrp_attribute_lifecycle) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create an MSRP talker attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + pwtest_ptr_notnull(attr->mrp); + + /* Begin and join the attribute */ + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Tick to process the MRP state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: server with Milan v1.2 mode. + */ +PWTEST(avb_milan_server_create) +{ + struct impl *impl; + struct server *server; + + impl = test_impl_new(); + + /* Create a Milan-mode server manually */ + server = calloc(1, sizeof(*server)); + pwtest_ptr_notnull(server); + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_MILAN_V12; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + + pwtest_int_eq(server->transport->setup(server), 0); + + server->mrp = avb_mrp_new(server); + pwtest_ptr_notnull(server->mrp); + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add minimal entity descriptor (skip init_descriptors which needs pw_core) */ + { + struct avb_aem_desc_entity entity; + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(server->entity_id); + entity.entity_model_id = htobe64(0x0001000000000001ULL); + entity.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + } + + /* Verify Milan mode was set correctly */ + pwtest_str_eq(get_avb_mode_str(server->avb_mode), "Milan V1.2"); + + /* Tick to exercise periodic handlers with Milan descriptors */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MRP State Machine Tests + * ===================================================================== + */ + +/* + * Test: MRP attribute begin sets initial state, join(new=true) enables + * pending_send after TX event via periodic tick. + */ +PWTEST(avb_mrp_begin_join_new_tx) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create a talker attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + /* After begin, pending_send should be 0 */ + avb_mrp_attribute_begin(attr->mrp, 0); + pwtest_int_eq(attr->mrp->pending_send, 0); + + /* Join with new=true */ + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Tick to let timers initialize (first periodic skips events) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick past join timer (100ms) to trigger TX event */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* After TX, pending_send should be set (NEW=0 encoded as non-zero + * only if the state machine decided to send). The VN state on TX + * produces SEND_NEW. But pending_send is only written if joined=true. */ + /* We mainly verify no crash and that the state machine ran. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP attribute join then leave cycle. + * After leave, the attribute should eventually stop sending. + */ +PWTEST(avb_mrp_join_leave_cycle) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Let the state machine run a few cycles */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* Now leave */ + avb_mrp_attribute_leave(attr->mrp, 2 * SPA_NSEC_PER_SEC); + + /* After leave, pending_send should reflect leaving state. + * The next TX event should send LV and transition to VO. */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* After the TX, pending_send should be 0 (joined is false, + * so pending_send is not updated by the state machine). */ + pwtest_int_eq(attr->mrp->pending_send, 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP attribute receives RX_NEW, which triggers a registrar + * notification (NOTIFY_NEW). Verify via a notification tracker. + */ +struct notify_tracker { + int new_count; + int join_count; + int leave_count; + uint8_t last_notify; +}; + +static void track_mrp_notify(void *data, uint64_t now, + struct avb_mrp_attribute *attr, uint8_t notify) +{ + struct notify_tracker *t = data; + t->last_notify = notify; + switch (notify) { + case AVB_MRP_NOTIFY_NEW: + t->new_count++; + break; + case AVB_MRP_NOTIFY_JOIN: + t->join_count++; + break; + case AVB_MRP_NOTIFY_LEAVE: + t->leave_count++; + break; + } +} + +static const struct avb_mrp_events test_mrp_events = { + AVB_VERSION_MRP_EVENTS, + .notify = track_mrp_notify, +}; + +PWTEST(avb_mrp_rx_new_notification) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Register a global MRP listener to track notifications */ + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Simulate receiving NEW from a peer */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + + /* RX_NEW should trigger NOTIFY_NEW on the registrar */ + pwtest_int_eq(tracker.new_count, 1); + pwtest_int_eq(tracker.last_notify, AVB_MRP_NOTIFY_NEW); + + /* Simulate receiving JOININ from a peer (already IN, no new notification) */ + avb_mrp_attribute_rx_event(attr->mrp, 2 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_JOININ); + /* Registrar was already IN, so no additional JOIN notification */ + pwtest_int_eq(tracker.join_count, 0); + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP registrar leave timer — after RX_LV, the registrar enters + * LV state. After MRP_LVTIMER_MS (1000ms), LV_TIMER fires and + * registrar transitions to MT with NOTIFY_LEAVE. + */ +PWTEST(avb_mrp_registrar_leave_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Get registrar to IN state via RX_NEW */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + pwtest_int_eq(tracker.new_count, 1); + + /* RX_LV transitions registrar IN -> LV, sets leave_timeout */ + avb_mrp_attribute_rx_event(attr->mrp, 2 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_LV); + + /* Tick before the leave timer expires — no LEAVE notification yet */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 500 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(tracker.leave_count, 0); + + /* Tick after the leave timer expires (1000ms after RX_LV at 2s = 3s) */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(tracker.leave_count, 1); + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Multiple MRP attributes coexist — events applied to all. + */ +PWTEST(avb_mrp_multiple_attributes) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr1, *attr2; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr1 = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + attr2 = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + pwtest_ptr_notnull(attr1); + pwtest_ptr_notnull(attr2); + + avb_mrp_attribute_begin(attr1->mrp, 0); + avb_mrp_attribute_join(attr1->mrp, 0, true); + + avb_mrp_attribute_begin(attr2->mrp, 0); + avb_mrp_attribute_join(attr2->mrp, 0, false); + + /* Periodic tick should apply to both attributes without crash */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MSRP Tests + * ===================================================================== + */ + +/* + * Test: Create each MSRP attribute type and verify fields. + */ +PWTEST(avb_msrp_attribute_types) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker, *talker_fail, *listener_attr, *domain; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create all four MSRP attribute types */ + talker = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(talker); + pwtest_int_eq(talker->type, AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + pwtest_int_eq(talker_fail->type, AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + + listener_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + pwtest_ptr_notnull(listener_attr); + pwtest_int_eq(listener_attr->type, AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + + domain = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + pwtest_ptr_notnull(domain); + pwtest_int_eq(domain->type, AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + + /* Configure talker with stream parameters */ + talker->attr.talker.stream_id = htobe64(0x020000fffe000001ULL); + talker->attr.talker.vlan_id = htons(AVB_DEFAULT_VLAN); + talker->attr.talker.tspec_max_frame_size = htons(256); + talker->attr.talker.tspec_max_interval_frames = htons( + AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT); + talker->attr.talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + talker->attr.talker.rank = AVB_MSRP_RANK_DEFAULT; + + /* Configure listener for same stream */ + listener_attr->attr.listener.stream_id = htobe64(0x020000fffe000001ULL); + listener_attr->param = AVB_MSRP_LISTENER_PARAM_READY; + + /* Begin and join all attributes */ + avb_mrp_attribute_begin(talker->mrp, 0); + avb_mrp_attribute_join(talker->mrp, 0, true); + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + avb_mrp_attribute_begin(listener_attr->mrp, 0); + avb_mrp_attribute_join(listener_attr->mrp, 0, true); + avb_mrp_attribute_begin(domain->mrp, 0); + avb_mrp_attribute_join(domain->mrp, 0, true); + + /* Tick to exercise all attribute types through the state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP domain attribute encode/transmit via loopback. + * After join+TX, the domain attribute should produce a packet. + */ +PWTEST(avb_msrp_domain_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *domain; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* The test server already has a domain_attr, but create another + * to test independent domain attribute behavior */ + domain = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + domain->attr.domain.sr_class_id = 7; + domain->attr.domain.sr_class_priority = 2; + domain->attr.domain.sr_class_vid = htons(100); + + avb_mrp_attribute_begin(domain->mrp, 0); + avb_mrp_attribute_join(domain->mrp, 0, true); + + /* Let timers initialize and then trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* MSRP should have transmitted a packet with domain data */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker advertise encode/transmit via loopback. + */ +PWTEST(avb_msrp_talker_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker; + uint64_t stream_id = 0x020000fffe000001ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + talker = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(talker); + + talker->attr.talker.stream_id = htobe64(stream_id); + talker->attr.talker.vlan_id = htons(AVB_DEFAULT_VLAN); + talker->attr.talker.tspec_max_frame_size = htons(256); + talker->attr.talker.tspec_max_interval_frames = htons(1); + talker->attr.talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + talker->attr.talker.rank = AVB_MSRP_RANK_DEFAULT; + + avb_mrp_attribute_begin(talker->mrp, 0); + avb_mrp_attribute_join(talker->mrp, 0, true); + + /* Let timers initialize */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* Should have transmitted the talker advertise */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + /* Read the packet and verify it contains valid MSRP data */ + { + uint8_t buf[2048]; + int len; + struct avb_packet_mrp *mrp_pkt; + + len = avb_loopback_get_packet(server, buf, sizeof(buf)); + pwtest_int_gt(len, (int)sizeof(struct avb_packet_mrp)); + + mrp_pkt = (struct avb_packet_mrp *)buf; + pwtest_int_eq(mrp_pkt->version, AVB_MRP_PROTOCOL_VERSION); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MRP Packet Parsing Tests + * ===================================================================== + */ + +struct parse_tracker { + int check_header_count; + int attr_event_count; + int process_count; + uint8_t last_attr_type; + uint8_t last_event; + uint8_t last_param; +}; + +static bool test_check_header(void *data, const void *hdr, + size_t *hdr_size, bool *has_params) +{ + struct parse_tracker *t = data; + const struct avb_packet_mrp_hdr *h = hdr; + t->check_header_count++; + + /* Accept attribute types 1-4 (MSRP-like) */ + if (h->attribute_type < 1 || h->attribute_type > 4) + return false; + + *hdr_size = sizeof(struct avb_packet_msrp_msg); + *has_params = (h->attribute_type == AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + return true; +} + +static int test_attr_event(void *data, uint64_t now, + uint8_t attribute_type, uint8_t event) +{ + struct parse_tracker *t = data; + t->attr_event_count++; + return 0; +} + +static int test_process(void *data, uint64_t now, + uint8_t attribute_type, const void *value, + uint8_t event, uint8_t param, int index) +{ + struct parse_tracker *t = data; + t->process_count++; + t->last_attr_type = attribute_type; + t->last_event = event; + t->last_param = param; + return 0; +} + +static const struct avb_mrp_parse_info test_parse_info = { + AVB_VERSION_MRP_PARSE_INFO, + .check_header = test_check_header, + .attr_event = test_attr_event, + .process = test_process, +}; + +/* + * Test: Parse a minimal MRP packet with a single domain value. + */ +PWTEST(avb_mrp_parse_single_domain) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + /* Build MRP packet manually: + * [ethernet header + version] already at offset 0 */ + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + /* MSRP message header for domain (type=4, length=4) */ + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + /* Event byte: 1 value, event=JOININ(1), packed as 1*36 = 36 */ + ev = (uint8_t *)(d + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_JOININ * 36; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); /* +2 for vector end mark */ + + /* Vector end mark */ + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + + /* Attribute end mark */ + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.check_header_count, 1); + pwtest_int_eq(tracker.process_count, 1); + pwtest_int_eq(tracker.last_attr_type, AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_JOININ); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Parse MRP packet with LVA (leave-all) flag set. + */ +PWTEST(avb_mrp_parse_with_lva) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 1; /* Set LVA flag */ + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + ev = (uint8_t *)(d + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_NEW * 36; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.check_header_count, 1); + pwtest_int_eq(tracker.attr_event_count, 1); /* LVA event fired */ + pwtest_int_eq(tracker.process_count, 1); + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_NEW); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Parse MRP packet with multiple values (3 values per event byte). + * Verifies the base-6 event decoding logic. + */ +PWTEST(avb_mrp_parse_three_values) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + uint8_t ev0 = AVB_MRP_ATTRIBUTE_EVENT_NEW; /* 0 */ + uint8_t ev1 = AVB_MRP_ATTRIBUTE_EVENT_JOININ; /* 1 */ + uint8_t ev2 = AVB_MRP_ATTRIBUTE_EVENT_MT; /* 4 */ + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 3); + + /* First value (domain data) — all 3 values share the same + * first_value pointer in the parse callback */ + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + /* Pack 3 events into 1 byte: ev0*36 + ev1*6 + ev2 */ + ev = (uint8_t *)(d + 1); + *ev = ev0 * 36 + ev1 * 6 + ev2; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.process_count, 3); + /* The last value processed should have event MT (4) */ + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_MT); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker-failed attribute with notification. + * This tests the NULL notify crash that was fixed. + */ +PWTEST(avb_msrp_talker_failed_notify) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker_fail; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + + talker_fail->attr.talker_fail.talker.stream_id = + htobe64(0x020000fffe000001ULL); + talker_fail->attr.talker_fail.failure_code = AVB_MRP_FAIL_BANDWIDTH; + + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + + /* Simulate receiving NEW from a peer — this triggers NOTIFY_NEW + * which calls msrp_notify -> dispatch[TALKER_FAILED].notify. + * Before the fix, this would crash with NULL pointer dereference. */ + avb_mrp_attribute_rx_event(talker_fail->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + + /* If we get here without crashing, the NULL check fix works */ + + /* Also exercise periodic to verify full lifecycle */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 4: ACMP Integration Tests + * ===================================================================== + */ + +/** + * Build an ACMP packet for injection into a server. + * Returns packet size, or -1 on error. + */ +static int avb_test_build_acmp(uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint8_t message_type, + uint64_t controller_guid, + uint64_t talker_guid, + uint64_t listener_guid, + uint16_t talker_unique_id, + uint16_t listener_unique_id, + uint16_t sequence_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_acmp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t acmp_mac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, acmp_mac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_acmp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ACMP); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(p, message_type); + AVB_PACKET_ACMP_SET_STATUS(p, AVB_ACMP_STATUS_SUCCESS); + p->controller_guid = htobe64(controller_guid); + p->talker_guid = htobe64(talker_guid); + p->listener_guid = htobe64(listener_guid); + p->talker_unique_id = htons(talker_unique_id); + p->listener_unique_id = htons(listener_unique_id); + p->sequence_id = htons(sequence_id); + + return len; +} + +/* + * Test: ACMP GET_TX_STATE_COMMAND should respond with NOT_SUPPORTED. + */ +PWTEST(avb_acmp_not_supported) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x10 }; + uint64_t remote_entity_id = 0x020000fffe000010ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send GET_TX_STATE_COMMAND to our server as talker */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + 0, /* listener */ + 0, 0, 42); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Server should respond with NOT_SUPPORTED */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + /* Read response and verify it's a GET_TX_STATE_RESPONSE with NOT_SUPPORTED */ + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_NOT_SUPPORTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP CONNECT_TX_COMMAND to our server with no streams + * should respond with TALKER_NO_STREAM_INDEX. + */ +PWTEST(avb_acmp_connect_tx_no_stream) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x11 }; + uint64_t remote_entity_id = 0x020000fffe000011ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send CONNECT_TX_COMMAND — we have no streams configured */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + remote_entity_id, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond with CONNECT_TX_RESPONSE + TALKER_NO_STREAM_INDEX */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP message addressed to a different entity_id is ignored. + */ +PWTEST(avb_acmp_wrong_entity_ignored) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x12 }; + uint64_t other_entity = 0xDEADBEEFCAFE0001ULL; + uint64_t controller_entity = 0x020000fffe000012ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* CONNECT_TX_COMMAND addressed to a different talker — should be ignored */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, + controller_entity, + other_entity, /* talker = NOT us */ + controller_entity, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* No response should be sent since the GUID doesn't match */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* CONNECT_RX_COMMAND addressed to a different listener — also ignored */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + other_entity, /* talker */ + other_entity, /* listener = NOT us */ + 0, 0, 2); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Still no response */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP CONNECT_RX_COMMAND to our server as listener. + * Should create a pending request and forward CONNECT_TX_COMMAND to talker. + */ +PWTEST(avb_acmp_connect_rx_forward) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x20 }; + uint64_t controller_entity = 0x020000fffe000020ULL; + uint64_t talker_entity = 0x020000fffe000030ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send CONNECT_RX_COMMAND to us as listener */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + talker_entity, /* talker = remote */ + server->entity_id, /* listener = us */ + 0, 0, 100); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* We should have forwarded a CONNECT_TX_COMMAND to the talker */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *cmd; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + cmd = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(cmd), + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP pending timeout and retry behavior. + * After CONNECT_RX_COMMAND, the listener creates a pending request. + * After timeout (2000ms for CONNECT_TX), it should retry once. + * After second timeout, it should be cleaned up. + */ +PWTEST(avb_acmp_pending_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x21 }; + uint64_t controller_entity = 0x020000fffe000021ULL; + uint64_t talker_entity = 0x020000fffe000031ULL; + int pkt_count_after_forward; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Create a pending request via CONNECT_RX_COMMAND */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + talker_entity, + server->entity_id, + 0, 0, 200); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Count packets after initial forward */ + pkt_count_after_forward = avb_loopback_get_packet_count(server); + pwtest_int_gt(pkt_count_after_forward, 0); + + /* Drain the packet queue */ + avb_loopback_clear_packets(server); + + /* Tick before timeout (2000ms) — no retry yet */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Tick after timeout (1s + 2000ms = 3s) — should retry */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Tick after second timeout — should give up (no more retries) */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + /* The pending was freed, no more retries */ + + /* Tick again — should be clean, no crashes */ + avb_test_tick(server, 6 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP message with wrong EtherType or subtype is filtered. + */ +PWTEST(avb_acmp_packet_filtering) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x13 }; + struct avb_ethernet_header *h; + struct avb_packet_acmp *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build a valid-looking ACMP packet but with wrong EtherType */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 1); + h = (struct avb_ethernet_header *)pkt; + h->type = htons(0x1234); /* Wrong EtherType */ + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Build packet with wrong subtype */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 2); + p = (struct avb_packet_acmp *)(pkt + sizeof(struct avb_ethernet_header)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); /* Wrong subtype */ + + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Build packet with correct parameters — should get response */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 3); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 5: AECP/AEM Entity Model Tests + * ===================================================================== + */ + +/* + * Test: AECP READ_DESCRIPTOR for the entity descriptor. + * Verifies that a valid READ_DESCRIPTOR command returns SUCCESS + * with the entity descriptor data. + */ +PWTEST(avb_aecp_read_descriptor_entity) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x40 }; + uint64_t controller_id = 0x020000fffe000040ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.configuration = 0; + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get a response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem))); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + + /* Should be AEM_RESPONSE with SUCCESS */ + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Response should include the descriptor data, making it + * larger than just the header + read_descriptor payload */ + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem) + + sizeof(struct avb_packet_aecp_aem_read_descriptor))); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP READ_DESCRIPTOR for a non-existent descriptor. + * Should return NO_SUCH_DESCRIPTOR error. + */ +PWTEST(avb_aecp_read_descriptor_not_found) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x41 }; + uint64_t controller_id = 0x020000fffe000041ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request a descriptor type that doesn't exist */ + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP message filtering — wrong EtherType and subtype. + */ +PWTEST(avb_aecp_packet_filtering) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x42 }; + uint64_t controller_id = 0x020000fffe000042ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + struct avb_ethernet_header *h; + struct avb_packet_aecp_aem *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + /* Wrong EtherType — should be filtered */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + h = (struct avb_ethernet_header *)pkt; + h->type = htons(0x1234); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Wrong subtype — should be filtered */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 2, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + p = SPA_PTROFF(pkt, sizeof(struct avb_ethernet_header), void); + AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_ADP); + + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Correct packet — should get a response */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 3, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP unsupported message types (ADDRESS_ACCESS, AVC, VENDOR_UNIQUE). + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_unsupported_message_types) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x43 }; + uint64_t controller_id = 0x020000fffe000043ULL; + struct avb_packet_aecp_aem *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build a basic AECP packet, then change message type to ADDRESS_ACCESS */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + NULL, 0); + + p = SPA_PTROFF(pkt, sizeof(struct avb_ethernet_header), void); + AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, + AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_header *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(resp), + AVB_AECP_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AEM command not in the legacy command table. + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_aem_not_implemented) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x44 }; + uint64_t controller_id = 0x020000fffe000044ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* REBOOT command is not in the legacy table */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_REBOOT, + NULL, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP ACQUIRE_ENTITY (legacy) with valid entity descriptor. + * Tests the fix for the pointer offset bug in handle_acquire_entity_avb_legacy. + */ +PWTEST(avb_aecp_acquire_entity_legacy) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x45 }; + uint64_t controller_id = 0x020000fffe000045ULL; + struct avb_packet_aecp_aem_acquire acq; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Acquire the entity descriptor */ + memset(&acq, 0, sizeof(acq)); + acq.flags = 0; + acq.owner_guid = htobe64(controller_id); + acq.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + acq.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, + &acq, sizeof(acq)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP LOCK_ENTITY (legacy) with valid entity descriptor. + * Tests the fix for the pointer offset bug in handle_lock_entity_avb_legacy. + */ +PWTEST(avb_aecp_lock_entity_legacy) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x46 }; + uint64_t controller_id = 0x020000fffe000046ULL; + struct avb_packet_aecp_aem_acquire lock_pkt; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Lock the entity descriptor (lock uses same struct as acquire) */ + memset(&lock_pkt, 0, sizeof(lock_pkt)); + lock_pkt.flags = 0; + lock_pkt.owner_guid = htobe64(controller_id); + lock_pkt.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + lock_pkt.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_pkt, sizeof(lock_pkt)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan ENTITY_AVAILABLE command. + * Verifies the entity available handler returns lock status. + */ +PWTEST(avb_aecp_entity_available_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x47 }; + uint64_t controller_id = 0x020000fffe000047ULL; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* ENTITY_AVAILABLE has no payload */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ENTITY_AVAILABLE, + NULL, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan LOCK_ENTITY — lock, verify locked, unlock. + * Tests lock semantics and the reply_status pointer fix. + */ +PWTEST(avb_aecp_lock_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x48 }; + uint64_t controller_id = 0x020000fffe000048ULL; + uint64_t other_controller = 0x020000fffe000049ULL; + struct avb_packet_aecp_aem_lock lock_payload; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* Lock the entity */ + memset(&lock_payload, 0, sizeof(lock_payload)); + lock_payload.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + lock_payload.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS for the lock */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + /* Another controller tries to lock — should get ENTITY_LOCKED */ + avb_loopback_clear_packets(server); + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, other_controller, 2, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_ENTITY_LOCKED); + } + + /* Original controller unlocks */ + avb_loopback_clear_packets(server); + lock_payload.flags = htonl(AECP_AEM_LOCK_ENTITY_FLAG_UNLOCK); + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 3, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan LOCK_ENTITY for non-entity descriptor returns NOT_SUPPORTED. + */ +PWTEST(avb_aecp_lock_non_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4A }; + uint64_t controller_id = 0x020000fffe00004AULL; + struct avb_packet_aecp_aem_lock lock_payload; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* Try to lock AUDIO_UNIT descriptor (not entity) */ + memset(&lock_payload, 0, sizeof(lock_payload)); + lock_payload.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + lock_payload.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get NO_SUCH_DESCRIPTOR (audio_unit doesn't exist) */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + /* Bug fix verified: reply_status now gets the full frame pointer, + * so the response is correctly formed */ + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan ACQUIRE_ENTITY returns NOT_SUPPORTED. + * Milan v1.2 does not implement acquire. + */ +PWTEST(avb_aecp_acquire_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4B }; + uint64_t controller_id = 0x020000fffe00004BULL; + struct avb_packet_aecp_aem_acquire acq; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + memset(&acq, 0, sizeof(acq)); + acq.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + acq.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, + &acq, sizeof(acq)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_SUPPORTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan READ_DESCRIPTOR works the same as legacy. + */ +PWTEST(avb_aecp_read_descriptor_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4C }; + uint64_t controller_id = 0x020000fffe00004CULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 7: Additional Protocol Coverage Tests + * ===================================================================== + */ + +/* + * Test: MAAP conflict detection — verify the 4 overlap cases in + * maap_check_conflict(). MAAP uses the pool 91:e0:f0:00:xx:xx, + * so only the last 2 bytes (offset) matter for overlap checks. + * + * We can't call maap_check_conflict() directly (it's static), but + * we can test the MAAP state machine via packet injection. + * When a PROBE packet is received that conflicts with our reservation + * in ANNOUNCE state, the server should send a DEFEND packet. + * When in PROBE state, a conflict causes re-probing (new address). + */ +PWTEST(avb_maap_conflict_probe_in_announce) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x50 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* avb_maap_reserve(server->maap, 1) was called in avb_test_server_new + * via the test-avb-utils.h helper (through init of domain_attr etc). + * The MAAP starts in STATE_PROBE. We need to advance it to STATE_ANNOUNCE + * by ticking through the probe retransmits (3 probes). */ + + /* Tick through probe retransmits — probe_count starts at 3, + * each tick past timeout sends a probe and decrements. + * PROBE_INTERVAL_MS = 500, so tick at 600ms intervals. + * After 3 probes, state transitions to ANNOUNCE. */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 4 * SPA_NSEC_PER_SEC); + + avb_loopback_clear_packets(server); + + /* Build a MAAP PROBE packet that overlaps with our reserved range. + * We use the base pool address with our server's MAAP offset. + * Since we can't read the internal offset, we use the full pool + * range to guarantee overlap. */ + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_PROBE); + + /* Request the entire pool — guaranteed to overlap with any reservation */ + AVB_PACKET_MAAP_SET_REQUEST_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, 0xFE00); + + /* Inject — in ANNOUNCE state, a conflicting PROBE should trigger DEFEND */ + avb_test_inject_packet(server, 5 * SPA_NSEC_PER_SEC, pkt, len); + + /* The server should NOT crash. If it was in ANNOUNCE state, + * it sends a DEFEND. If still in PROBE, it picks a new address. + * Either way, the conflict detection logic was exercised. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP DEFEND packet causes re-probing when conflict overlaps + * with our address during PROBE state. + */ +PWTEST(avb_maap_defend_causes_reprobe) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x51 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* MAAP is in PROBE state after reserve. Send a DEFEND packet + * with conflict range covering the entire pool. */ + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_DEFEND); + + /* Set conflict range to cover the whole pool */ + AVB_PACKET_MAAP_SET_CONFLICT_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, 0xFE00); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should have re-probed — exercise the state machine without crash */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP ANNOUNCE packet with conflict triggers re-probe. + * ANNOUNCE is handled via handle_defend() in the code. + */ +PWTEST(avb_maap_announce_conflict) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x52 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_ANNOUNCE); + + /* Conflict range covers entire pool */ + AVB_PACKET_MAAP_SET_CONFLICT_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, 0xFE00); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP no-conflict — PROBE packet with non-overlapping range. + */ +PWTEST(avb_maap_no_conflict) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x53 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_PROBE); + + /* Use a different base prefix — won't match (memcmp of first 4 bytes fails) */ + AVB_PACKET_MAAP_SET_REQUEST_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf1, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, 1); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* No conflict — no DEFEND should be sent (even if in ANNOUNCE state) */ + /* We can't check packet count reliably since MAAP uses send() on its + * own fd, not the loopback transport. But the path was exercised. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP DISCONNECT_RX_COMMAND flow. + * Send DISCONNECT_RX_COMMAND to our server as listener. + * Should forward DISCONNECT_TX_COMMAND to the talker. + */ +PWTEST(avb_acmp_disconnect_rx_forward) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x60 }; + uint64_t controller_entity = 0x020000fffe000060ULL; + uint64_t talker_entity = 0x020000fffe000070ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send DISCONNECT_RX_COMMAND to us as listener */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, + controller_entity, + talker_entity, /* talker = remote */ + server->entity_id, /* listener = us */ + 0, 0, 300); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should forward a DISCONNECT_TX_COMMAND to the talker */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *cmd; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + cmd = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(cmd), + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP DISCONNECT_TX_COMMAND to our server as talker with no streams. + * Should respond with TALKER_NO_STREAM_INDEX. + */ +PWTEST(avb_acmp_disconnect_tx_no_stream) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x61 }; + uint64_t remote_entity_id = 0x020000fffe000061ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send DISCONNECT_TX_COMMAND — we have no streams */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + remote_entity_id, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond with DISCONNECT_TX_RESPONSE + TALKER_NO_STREAM_INDEX */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + struct avb_packet_acmp *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP disconnect pending timeout. + * DISCONNECT_TX_COMMAND timeout is 200ms, much shorter than CONNECT_TX (2000ms). + * After DISCONNECT_RX_COMMAND, the pending should timeout faster. + */ +PWTEST(avb_acmp_disconnect_pending_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x62 }; + uint64_t controller_entity = 0x020000fffe000062ULL; + uint64_t talker_entity = 0x020000fffe000072ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Create pending via DISCONNECT_RX_COMMAND */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, + controller_entity, + talker_entity, + server->entity_id, + 0, 0, 400); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + avb_loopback_clear_packets(server); + + /* Tick before timeout (200ms) — no retry yet */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Tick after timeout (200ms) — should retry */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 300 * SPA_NSEC_PER_MSEC); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Tick again after second timeout — should be freed */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + /* No crash — pending was cleaned up */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_AVB_INFO command with AVB_INTERFACE descriptor. + * Adds an AVB_INTERFACE descriptor, injects GET_AVB_INFO, and + * verifies the response contains gptp_grandmaster_id and domain_number. + */ +PWTEST(avb_aecp_get_avb_info) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x70 }; + uint64_t controller_id = 0x020000fffe000070ULL; + struct avb_packet_aecp_aem_get_avb_info avb_info_req; + uint64_t test_clock_id = 0x0200000000000042ULL; + uint8_t test_domain = 7; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add an AVB_INTERFACE descriptor to the server */ + { + struct avb_aem_desc_avb_interface avb_iface; + memset(&avb_iface, 0, sizeof(avb_iface)); + avb_iface.clock_identity = htobe64(test_clock_id); + avb_iface.domain_number = test_domain; + avb_iface.interface_flags = htons( + AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED); + server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0, + sizeof(avb_iface), &avb_iface); + } + + /* Build GET_AVB_INFO command */ + memset(&avb_info_req, 0, sizeof(avb_info_req)); + avb_info_req.descriptor_type = htons(AVB_AEM_DESC_AVB_INTERFACE); + avb_info_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_AVB_INFO, + &avb_info_req, sizeof(avb_info_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get a SUCCESS response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + struct avb_packet_aecp_aem_get_avb_info *info; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem))); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Verify the response payload */ + info = (struct avb_packet_aecp_aem_get_avb_info *)resp->payload; + pwtest_int_eq(be64toh(info->gptp_grandmaster_id), (int64_t)test_clock_id); + pwtest_int_eq(info->gptp_domain_number, test_domain); + pwtest_int_eq(ntohl(info->propagation_delay), 0); + pwtest_int_eq(ntohs(info->msrp_mappings_count), 0); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_AVB_INFO with wrong descriptor type returns NOT_IMPLEMENTED. + * The handler requires AVB_AEM_DESC_AVB_INTERFACE specifically. + */ +PWTEST(avb_aecp_get_avb_info_wrong_type) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x71 }; + uint64_t controller_id = 0x020000fffe000071ULL; + struct avb_packet_aecp_aem_get_avb_info avb_info_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request GET_AVB_INFO for entity descriptor (wrong type) */ + memset(&avb_info_req, 0, sizeof(avb_info_req)); + avb_info_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + avb_info_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_AVB_INFO, + &avb_info_req, sizeof(avb_info_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get NOT_IMPLEMENTED (descriptor exists but is wrong type) */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP leave-all timer fires and triggers global LVA event. + * After LVA_TIMER_MS (10000ms), the leave-all timer fires, sending + * RX_LVA to all attributes and setting leave_all=true for the next TX. + */ +PWTEST(avb_mrp_leave_all_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + /* Create and join an attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Get registrar to IN state */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + pwtest_int_eq(tracker.new_count, 1); + + /* Initialize timers with first tick */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick at various times before LVA timeout — no leave-all yet */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 9 * SPA_NSEC_PER_SEC); + + /* Tick past LVA timeout (10000ms from the first tick at 1s = 11s) */ + avb_test_tick(server, 12 * SPA_NSEC_PER_SEC); + + /* The LVA event should have been processed without crash. + * The TX_LVA event is combined with the join timer TX, + * which may produce SEND_LVA type transmissions. */ + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP periodic timer fires at 1000ms intervals. + * The periodic event is applied globally to all attributes. + */ +PWTEST(avb_mrp_periodic_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* First tick initializes timers */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick just before periodic timeout (1000ms) — no periodic event yet */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 500 * SPA_NSEC_PER_MSEC); + + /* Tick past periodic timeout */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + + /* Tick multiple periodic intervals to exercise repeated timer */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 4 * SPA_NSEC_PER_SEC + 300 * SPA_NSEC_PER_MSEC); + + /* No crash — periodic timer logic works correctly */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker-failed processing via MRP packet parsing. + * Build an MSRP packet containing a talker-failed attribute and + * inject it. Verifies process_talker_fail() processes correctly. + */ +PWTEST(avb_msrp_talker_failed_process) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker_fail; + uint64_t stream_id = 0x020000fffe000080ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create a talker-failed attribute that matches the stream_id + * we'll send in the MSRP packet. This ensures process_talker_fail() + * finds a matching attribute and calls avb_mrp_attribute_rx_event(). */ + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + + talker_fail->attr.talker_fail.talker.stream_id = htobe64(stream_id); + talker_fail->attr.talker_fail.failure_code = AVB_MRP_FAIL_BANDWIDTH; + + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + + /* Build an MSRP packet with a talker-failed message. + * The MSRP packet parser will dispatch to process_talker_fail() + * when it sees attribute_type = TALKER_FAILED. */ + { + uint8_t buf[512]; + int pos = 0; + struct avb_packet_mrp *mrp_pkt; + struct avb_packet_msrp_msg *msg; + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_talker_fail *tf; + struct avb_packet_mrp_footer *f; + uint8_t *ev; + size_t attr_list_length; + + memset(buf, 0, sizeof(buf)); + + /* MRP header */ + mrp_pkt = (struct avb_packet_mrp *)buf; + mrp_pkt->version = AVB_MRP_PROTOCOL_VERSION; + /* Fill in the ethernet header part */ + { + static const uint8_t msrp_mac[6] = { 0x91, 0xe0, 0xf0, 0x00, 0xe5, 0x00 }; + memcpy(mrp_pkt->eth.dest, msrp_mac, 6); + memcpy(mrp_pkt->eth.src, server->mac_addr, 6); + mrp_pkt->eth.type = htons(AVB_TSN_ETH); + } + pos = sizeof(struct avb_packet_mrp); + + /* MSRP talker-failed message */ + msg = (struct avb_packet_msrp_msg *)(buf + pos); + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED; + msg->attribute_length = sizeof(struct avb_packet_msrp_talker_fail); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + tf = (struct avb_packet_msrp_talker_fail *)v->first_value; + tf->talker.stream_id = htobe64(stream_id); + tf->talker.vlan_id = htons(AVB_DEFAULT_VLAN); + tf->talker.tspec_max_frame_size = htons(256); + tf->talker.tspec_max_interval_frames = htons(1); + tf->talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + tf->talker.rank = AVB_MSRP_RANK_DEFAULT; + tf->failure_code = AVB_MRP_FAIL_BANDWIDTH; + + ev = (uint8_t *)(tf + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_NEW * 36; /* single value, NEW event */ + + attr_list_length = sizeof(*v) + sizeof(*tf) + 1 + sizeof(*f); + msg->attribute_list_length = htons(attr_list_length); + + f = SPA_PTROFF(ev, 1, void); + f->end_mark = 0; + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*tf) + 1 + sizeof(*f); + + /* Attribute end mark */ + buf[pos++] = 0; + buf[pos++] = 0; + + /* Inject the MSRP packet */ + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, buf, pos); + } + + /* If we get here, process_talker_fail() was invoked without crash. + * The attribute's RX_NEW event would have been applied. */ + + /* Exercise periodic to verify ongoing stability */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 8: MVRP/MMRP, ADP Edge Cases, Descriptor, and AECP Command Tests + * ===================================================================== + */ + +/* + * Test: MVRP attribute creation and lifecycle. + * Create a VID attribute, begin, join, and exercise the state machine. + */ +PWTEST(avb_mvrp_attribute_lifecycle) +{ + struct impl *impl; + struct server *server; + struct avb_mvrp_attribute *vid; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + vid = avb_mvrp_attribute_new(server->mvrp, + AVB_MVRP_ATTRIBUTE_TYPE_VID); + pwtest_ptr_notnull(vid); + pwtest_int_eq(vid->type, AVB_MVRP_ATTRIBUTE_TYPE_VID); + + /* Configure VLAN ID */ + vid->attr.vid.vlan = htons(AVB_DEFAULT_VLAN); + + /* Begin and join */ + avb_mrp_attribute_begin(vid->mrp, 0); + avb_mrp_attribute_join(vid->mrp, 0, true); + + /* Tick through MRP state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MVRP VID attribute transmit via loopback. + * After join + TX timer, MVRP should encode and send a VID packet. + */ +PWTEST(avb_mvrp_vid_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_mvrp_attribute *vid; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + vid = avb_mvrp_attribute_new(server->mvrp, + AVB_MVRP_ATTRIBUTE_TYPE_VID); + pwtest_ptr_notnull(vid); + + vid->attr.vid.vlan = htons(100); + + avb_mrp_attribute_begin(vid->mrp, 0); + avb_mrp_attribute_join(vid->mrp, 0, true); + + /* Initialize timers */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* MVRP should have sent a packet */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MMRP attribute creation — both SERVICE_REQUIREMENT and MAC types. + */ +PWTEST(avb_mmrp_attribute_types) +{ + struct impl *impl; + struct server *server; + struct avb_mmrp_attribute *svc, *mac_attr; + static const uint8_t test_mac[6] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create service requirement attribute */ + svc = avb_mmrp_attribute_new(server->mmrp, + AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT); + pwtest_ptr_notnull(svc); + pwtest_int_eq(svc->type, AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT); + + memcpy(svc->attr.service_requirement.addr, test_mac, 6); + + /* Create MAC attribute */ + mac_attr = avb_mmrp_attribute_new(server->mmrp, + AVB_MMRP_ATTRIBUTE_TYPE_MAC); + pwtest_ptr_notnull(mac_attr); + pwtest_int_eq(mac_attr->type, AVB_MMRP_ATTRIBUTE_TYPE_MAC); + + memcpy(mac_attr->attr.mac.addr, test_mac, 6); + + /* Begin and join both */ + avb_mrp_attribute_begin(svc->mrp, 0); + avb_mrp_attribute_join(svc->mrp, 0, true); + avb_mrp_attribute_begin(mac_attr->mrp, 0); + avb_mrp_attribute_join(mac_attr->mrp, 0, true); + + /* Tick to exercise MRP state machine with both types */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP duplicate ENTITY_AVAILABLE is idempotent. + * Injecting the same entity_id twice should not create duplicate entries; + * last_time is updated. + */ +PWTEST(avb_adp_duplicate_entity_available) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x80 }; + uint64_t remote_entity_id = 0x020000fffe000080ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Inject entity available twice with same entity_id */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should not crash, and entity list should be consistent */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + /* Inject departing — only one entity to remove */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 4 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP targeted discover for a specific entity_id. + * Only the entity with matching ID should respond. + */ +PWTEST(avb_adp_targeted_discover) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x81 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Let the server advertise first */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Send targeted discover for our own entity */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, server->entity_id); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond since the entity_id matches ours */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Send targeted discover for a non-existent entity */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, 0xDEADBEEFCAFE0001ULL); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should NOT respond — entity doesn't exist */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP re-advertisement timing — the server should re-advertise + * at valid_time/2 intervals when ticked periodically. + */ +PWTEST(avb_adp_readvertise_timing) +{ + struct impl *impl; + struct server *server; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* First tick — should advertise (check_advertise creates entity) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Tick at 3s — too early for re-advertise (valid_time=10, re-adv at 5s) */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + /* Might or might not have packets depending on other protocols */ + + avb_loopback_clear_packets(server); + + /* Tick at 7s — past re-advertise interval (valid_time/2 = 5s from 1s = 6s) */ + avb_test_tick(server, 7 * SPA_NSEC_PER_SEC); + + /* Should have re-advertised */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP entity departure before timeout removes entity immediately. + */ +PWTEST(avb_adp_departure_before_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x82 }; + uint64_t remote_entity_id = 0x020000fffe000082ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add entity */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 30); /* long valid_time */ + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Immediate departing — before any timeout */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Entity should be removed immediately, not waiting for timeout */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + /* Re-add the same entity — should work if old one was properly removed */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 4 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Descriptor lookup edge cases — find existing, missing, multiple types. + */ +PWTEST(avb_descriptor_lookup_edge_cases) +{ + struct impl *impl; + struct server *server; + const struct descriptor *desc; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Entity descriptor should exist (added by avb_test_server_new) */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + pwtest_int_eq(desc->type, AVB_AEM_DESC_ENTITY); + pwtest_int_eq(desc->index, 0); + pwtest_int_gt((int)desc->size, 0); + + /* Non-existent descriptor type */ + desc = server_find_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0); + pwtest_ptr_null(desc); + + /* Non-existent index for existing type */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 1); + pwtest_ptr_null(desc); + + /* Add multiple descriptors and verify independent lookup */ + { + struct avb_aem_desc_avb_interface avb_iface; + memset(&avb_iface, 0, sizeof(avb_iface)); + server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0, + sizeof(avb_iface), &avb_iface); + } + + /* Both descriptors should be findable */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + desc = server_find_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0); + pwtest_ptr_notnull(desc); + + /* Invalid descriptor type still returns NULL */ + desc = server_find_descriptor(server, AVB_AEM_DESC_INVALID, 0); + pwtest_ptr_null(desc); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: server_add_descriptor with data copy — verify data is correctly + * stored and retrievable. + */ +PWTEST(avb_descriptor_data_integrity) +{ + struct impl *impl; + struct server *server; + const struct descriptor *desc; + struct avb_aem_desc_entity entity; + struct avb_aem_desc_entity *retrieved; + uint64_t test_entity_id = 0x0123456789ABCDEFULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Remove existing entity descriptor via server_destroy_descriptors + * and add a new one with known data */ + server_destroy_descriptors(server); + + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(test_entity_id); + entity.entity_model_id = htobe64(0x0001000000000002ULL); + entity.configurations_count = htons(2); + strncpy(entity.entity_name, "Test Entity", sizeof(entity.entity_name) - 1); + + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + + /* Retrieve and verify */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + pwtest_int_eq((int)desc->size, (int)sizeof(entity)); + + retrieved = desc->ptr; + pwtest_int_eq(be64toh(retrieved->entity_id), (int64_t)test_entity_id); + pwtest_int_eq(ntohs(retrieved->configurations_count), 2); + pwtest_int_eq(strncmp(retrieved->entity_name, "Test Entity", 11), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_CONFIGURATION command. + * Verify it returns the current_configuration from the entity descriptor. + */ +PWTEST(avb_aecp_get_configuration) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x90 }; + uint64_t controller_id = 0x020000fffe000090ULL; + struct avb_packet_aecp_aem_setget_configuration cfg_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build GET_CONFIGURATION command — no descriptor type/id needed, + * it always looks up ENTITY descriptor 0 internally */ + memset(&cfg_req, 0, sizeof(cfg_req)); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_CONFIGURATION, + &cfg_req, sizeof(cfg_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + struct avb_packet_aecp_aem_setget_configuration *cfg_resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Verify configuration_index is 0 (default) */ + cfg_resp = (struct avb_packet_aecp_aem_setget_configuration *)resp->payload; + pwtest_int_eq(ntohs(cfg_resp->configuration_index), 0); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_SAMPLING_RATE command. + * Add an AUDIO_UNIT descriptor with a known sampling rate and verify + * the response contains it. + */ +PWTEST(avb_aecp_get_sampling_rate) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x91 }; + uint64_t controller_id = 0x020000fffe000091ULL; + struct avb_packet_aecp_aem_setget_sampling_rate sr_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add an AUDIO_UNIT descriptor with a sampling rate */ + { + /* Allocate space for audio_unit + 1 sampling rate entry */ + uint8_t au_buf[sizeof(struct avb_aem_desc_audio_unit) + + sizeof(union avb_aem_desc_sampling_rate)]; + struct avb_aem_desc_audio_unit *au; + union avb_aem_desc_sampling_rate *sr; + + memset(au_buf, 0, sizeof(au_buf)); + au = (struct avb_aem_desc_audio_unit *)au_buf; + au->sampling_rates_count = htons(1); + au->sampling_rates_offset = htons(sizeof(*au)); + + /* Set current sampling rate to 48000 Hz + * pull_frequency is a uint32_t with frequency in bits [31:3] and pull in [2:0] */ + au->current_sampling_rate.pull_frequency = htonl(48000 << 3); + + /* Add one supported rate */ + sr = (union avb_aem_desc_sampling_rate *)(au_buf + sizeof(*au)); + sr->pull_frequency = htonl(48000 << 3); + + server_add_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0, + sizeof(au_buf), au_buf); + } + + /* Build GET_SAMPLING_RATE command */ + memset(&sr_req, 0, sizeof(sr_req)); + sr_req.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + sr_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, + &sr_req, sizeof(sr_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_SAMPLING_RATE with wrong descriptor type. + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_get_sampling_rate_wrong_type) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x92 }; + uint64_t controller_id = 0x020000fffe000092ULL; + struct avb_packet_aecp_aem_setget_sampling_rate sr_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request GET_SAMPLING_RATE for entity descriptor (wrong type) */ + memset(&sr_req, 0, sizeof(sr_req)); + sr_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + sr_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, + &sr_req, sizeof(sr_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_NAME for entity descriptor — retrieves entity_name. + */ +PWTEST(avb_aecp_get_name_entity) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x93 }; + uint64_t controller_id = 0x020000fffe000093ULL; + struct avb_packet_aecp_aem_setget_name name_req; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* GET_NAME for entity descriptor, name_index=0 (entity_name) */ + memset(&name_req, 0, sizeof(name_req)); + name_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + name_req.descriptor_index = htons(0); + name_req.name_index = htons(0); + name_req.configuration_index = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_NAME, + &name_req, sizeof(name_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_NAME with missing descriptor returns NO_SUCH_DESCRIPTOR. + */ +PWTEST(avb_aecp_get_name_missing_descriptor) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x94 }; + uint64_t controller_id = 0x020000fffe000094ULL; + struct avb_packet_aecp_aem_setget_name name_req; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* GET_NAME for AUDIO_UNIT which doesn't exist in test server */ + memset(&name_req, 0, sizeof(name_req)); + name_req.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + name_req.descriptor_index = htons(0); + name_req.name_index = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_NAME, + &name_req, sizeof(name_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 6: AVTP Audio Data Path Tests + * ===================================================================== + */ + +/* + * Test: Verify IEC61883 packet struct layout and size. + * The struct must be exactly 24 bytes (packed) for the header, + * followed by the flexible payload array. + */ +PWTEST(avb_iec61883_packet_layout) +{ + struct avb_packet_iec61883 pkt; + struct avb_frame_header fh; + + /* IEC61883 header (packed) with CIP fields = 32 bytes */ + pwtest_int_eq((int)sizeof(struct avb_packet_iec61883), 32); + + /* Frame header with 802.1Q tag should be 18 bytes */ + pwtest_int_eq((int)sizeof(struct avb_frame_header), 18); + + /* Total PDU header = frame_header + iec61883 = 50 bytes */ + pwtest_int_eq((int)(sizeof(fh) + sizeof(pkt)), 50); + + /* Verify critical field positions by setting and reading */ + memset(&pkt, 0, sizeof(pkt)); + pkt.subtype = AVB_SUBTYPE_61883_IIDC; + pwtest_int_eq(pkt.subtype, 0x00); + + pkt.sv = 1; + pkt.tv = 1; + pkt.seq_num = 42; + pkt.stream_id = htobe64(0x020000fffe000001ULL); + pkt.timestamp = htonl(1000000); + pkt.data_len = htons(200); + pkt.tag = 0x1; + pkt.channel = 0x1f; + pkt.tcode = 0xa; + pkt.sid = 0x3f; + pkt.dbs = 8; + pkt.qi2 = 0x2; + pkt.format_id = 0x10; + pkt.fdf = 0x2; + pkt.syt = htons(0x0008); + pkt.dbc = 0; + + /* Read back and verify */ + pwtest_int_eq(pkt.seq_num, 42); + pwtest_int_eq(pkt.dbs, 8); + pwtest_int_eq(be64toh(pkt.stream_id), (int64_t)0x020000fffe000001ULL); + pwtest_int_eq(ntohs(pkt.data_len), 200); + pwtest_int_eq((int)pkt.sv, 1); + pwtest_int_eq((int)pkt.tv, 1); + + return PWTEST_PASS; +} + +/* + * Test: Verify AAF packet struct layout. + */ +PWTEST(avb_aaf_packet_layout) +{ + struct avb_packet_aaf pkt; + + /* AAF header should be 24 bytes (same as IEC61883) */ + pwtest_int_eq((int)sizeof(struct avb_packet_aaf), 24); + + memset(&pkt, 0, sizeof(pkt)); + pkt.subtype = AVB_SUBTYPE_AAF; + pkt.sv = 1; + pkt.tv = 1; + pkt.seq_num = 99; + pkt.stream_id = htobe64(0x020000fffe000002ULL); + pkt.timestamp = htonl(2000000); + pkt.format = AVB_AAF_FORMAT_INT_24BIT; + pkt.nsr = AVB_AAF_PCM_NSR_48KHZ; + pkt.chan_per_frame = 8; + pkt.bit_depth = 24; + pkt.data_len = htons(192); /* 6 frames * 8 channels * 4 bytes */ + pkt.sp = AVB_AAF_PCM_SP_NORMAL; + + pwtest_int_eq(pkt.subtype, AVB_SUBTYPE_AAF); + pwtest_int_eq(pkt.seq_num, 99); + pwtest_int_eq(pkt.format, AVB_AAF_FORMAT_INT_24BIT); + pwtest_int_eq((int)pkt.nsr, AVB_AAF_PCM_NSR_48KHZ); + pwtest_int_eq(pkt.chan_per_frame, 8); + pwtest_int_eq(pkt.bit_depth, 24); + pwtest_int_eq(ntohs(pkt.data_len), 192); + + return PWTEST_PASS; +} + +/* + * Test: 802.1Q frame header construction for AVB. + */ +PWTEST(avb_frame_header_construction) +{ + struct avb_frame_header h; + static const uint8_t dest[6] = { 0x91, 0xe0, 0xf0, 0x00, 0x01, 0x00 }; + static const uint8_t src[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + int prio = 3; + int vlan_id = 2; + + memset(&h, 0, sizeof(h)); + memcpy(h.dest, dest, 6); + memcpy(h.src, src, 6); + h.type = htons(0x8100); /* 802.1Q VLAN tag */ + h.prio_cfi_id = htons((prio << 13) | vlan_id); + h.etype = htons(0x22f0); /* AVB/TSN EtherType */ + + /* Verify the 802.1Q header */ + pwtest_int_eq(ntohs(h.type), 0x8100); + pwtest_int_eq(ntohs(h.etype), 0x22f0); + + /* Extract priority from prio_cfi_id */ + pwtest_int_eq((ntohs(h.prio_cfi_id) >> 13) & 0x7, prio); + /* Extract VLAN ID (lower 12 bits) */ + pwtest_int_eq(ntohs(h.prio_cfi_id) & 0xFFF, vlan_id); + + return PWTEST_PASS; +} + +/* + * Test: PDU size calculations for various audio configurations. + * Verifies the math used in setup_pdu(). + */ +PWTEST(avb_pdu_size_calculations) +{ + size_t hdr_size, payload_size, pdu_size; + int64_t pdu_period; + + /* Default config: 8 channels, S24_32_BE (4 bytes), 6 frames/PDU, 48kHz */ + int channels = 8; + int sample_size = 4; /* S24_32_BE */ + int frames_per_pdu = 6; + int rate = 48000; + int stride = channels * sample_size; + + hdr_size = sizeof(struct avb_frame_header) + sizeof(struct avb_packet_iec61883); + payload_size = stride * frames_per_pdu; + pdu_size = hdr_size + payload_size; + pdu_period = SPA_NSEC_PER_SEC * frames_per_pdu / rate; + + /* Header: 18 (frame) + 32 (iec61883) = 50 bytes */ + pwtest_int_eq((int)hdr_size, 50); + + /* Payload: 8 ch * 4 bytes * 6 frames = 192 bytes */ + pwtest_int_eq((int)payload_size, 192); + + /* Total PDU: 50 + 192 = 242 bytes */ + pwtest_int_eq((int)pdu_size, 242); + + /* PDU period: 6/48000 seconds = 125000 ns = 125 us */ + pwtest_int_eq((int)pdu_period, 125000); + + /* Stride: 8 * 4 = 32 bytes per frame */ + pwtest_int_eq(stride, 32); + + /* IEC61883 data_len field = payload + 8 CIP header bytes */ + pwtest_int_eq((int)(payload_size + 8), 200); + + /* 2-channel configuration */ + channels = 2; + stride = channels * sample_size; + payload_size = stride * frames_per_pdu; + pwtest_int_eq((int)payload_size, 48); + pwtest_int_eq(stride, 8); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer audio data round-trip. + * Write audio frames to the ringbuffer, read them back, verify integrity. + */ +PWTEST(avb_ringbuffer_audio_roundtrip) +{ + struct spa_ringbuffer ring; + uint8_t buffer[BUFFER_SIZE]; + int stride = 32; /* 8 channels * 4 bytes */ + int frames = 48; /* 48 frames = 1ms at 48kHz */ + int n_bytes = frames * stride; + uint8_t write_data[2048]; + uint8_t read_data[2048]; + uint32_t index; + int32_t avail; + + spa_ringbuffer_init(&ring); + + /* Fill write_data with a recognizable pattern */ + for (int i = 0; i < n_bytes; i++) + write_data[i] = (uint8_t)(i & 0xFF); + + /* Write to ringbuffer */ + avail = spa_ringbuffer_get_write_index(&ring, &index); + pwtest_int_eq(avail, 0); + + spa_ringbuffer_write_data(&ring, buffer, sizeof(buffer), + index % sizeof(buffer), write_data, n_bytes); + index += n_bytes; + spa_ringbuffer_write_update(&ring, index); + + /* Read back from ringbuffer */ + avail = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(avail, n_bytes); + + spa_ringbuffer_read_data(&ring, buffer, sizeof(buffer), + index % sizeof(buffer), read_data, n_bytes); + index += n_bytes; + spa_ringbuffer_read_update(&ring, index); + + /* Verify data integrity */ + pwtest_int_eq(memcmp(write_data, read_data, n_bytes), 0); + + /* After read, buffer should be empty */ + avail = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(avail, 0); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer wrap-around behavior with multiple writes. + * Simulates multiple PDU-sized writes filling past the buffer end. + */ +PWTEST(avb_ringbuffer_wraparound) +{ + struct spa_ringbuffer ring; + uint8_t *buffer; + int stride = 32; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; /* 192 bytes */ + int num_writes = (BUFFER_SIZE / payload_size) + 5; /* Write past buffer end */ + uint8_t write_data[192]; + uint8_t read_data[192]; + uint32_t w_index, r_index; + int32_t avail; + + buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(buffer); + + spa_ringbuffer_init(&ring); + + /* Write many PDU payloads, reading as we go to prevent overrun */ + for (int i = 0; i < num_writes; i++) { + /* Fill with per-PDU pattern */ + memset(write_data, (uint8_t)(i + 1), payload_size); + + avail = spa_ringbuffer_get_write_index(&ring, &w_index); + spa_ringbuffer_write_data(&ring, buffer, BUFFER_SIZE, + w_index % BUFFER_SIZE, write_data, payload_size); + w_index += payload_size; + spa_ringbuffer_write_update(&ring, w_index); + + /* Read it back immediately */ + avail = spa_ringbuffer_get_read_index(&ring, &r_index); + pwtest_int_eq(avail, payload_size); + + spa_ringbuffer_read_data(&ring, buffer, BUFFER_SIZE, + r_index % BUFFER_SIZE, read_data, payload_size); + r_index += payload_size; + spa_ringbuffer_read_update(&ring, r_index); + + /* Verify the pattern survived the wrap-around */ + for (int j = 0; j < payload_size; j++) { + if (read_data[j] != (uint8_t)(i + 1)) { + free(buffer); + return PWTEST_FAIL; + } + } + } + + free(buffer); + + return PWTEST_PASS; +} + +/* + * Test: IEC61883 packet receive simulation. + * Builds IEC61883 packets and writes their payload into a ringbuffer, + * mirroring the logic of handle_iec61883_packet(). + */ +PWTEST(avb_iec61883_receive_simulation) +{ + struct spa_ringbuffer ring; + uint8_t *rb_buffer; + uint8_t pkt_buf[2048]; + struct avb_frame_header *h; + struct avb_packet_iec61883 *p; + int channels = 8; + int sample_size = 4; + int stride = channels * sample_size; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; /* 192 bytes */ + int n_packets = 10; + uint32_t index; + int32_t filled; + uint8_t read_data[192]; + + rb_buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(rb_buffer); + spa_ringbuffer_init(&ring); + + for (int i = 0; i < n_packets; i++) { + /* Build a receive packet like on_socket_data() would see */ + memset(pkt_buf, 0, sizeof(pkt_buf)); + h = (struct avb_frame_header *)pkt_buf; + p = SPA_PTROFF(h, sizeof(*h), void); + + p->subtype = AVB_SUBTYPE_61883_IIDC; + p->sv = 1; + p->tv = 1; + p->seq_num = i; + p->stream_id = htobe64(0x020000fffe000001ULL); + p->timestamp = htonl(i * 125000); + p->data_len = htons(payload_size + 8); /* payload + 8 CIP bytes */ + p->tag = 0x1; + p->dbs = channels; + p->dbc = i * frames_per_pdu; + + /* Fill payload with audio-like pattern */ + for (int j = 0; j < payload_size; j++) + p->payload[j] = (uint8_t)((i * payload_size + j) & 0xFF); + + /* Simulate handle_iec61883_packet() logic */ + { + int n_bytes = ntohs(p->data_len) - 8; + pwtest_int_eq(n_bytes, payload_size); + + filled = spa_ringbuffer_get_write_index(&ring, &index); + + if (filled + (int32_t)n_bytes <= (int32_t)BUFFER_SIZE) { + spa_ringbuffer_write_data(&ring, rb_buffer, BUFFER_SIZE, + index % BUFFER_SIZE, p->payload, n_bytes); + index += n_bytes; + spa_ringbuffer_write_update(&ring, index); + } + } + } + + /* Verify all packets were received */ + filled = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(filled, n_packets * payload_size); + + /* Read back first packet's data and verify */ + spa_ringbuffer_read_data(&ring, rb_buffer, BUFFER_SIZE, + index % BUFFER_SIZE, read_data, payload_size); + + for (int j = 0; j < payload_size; j++) { + if (read_data[j] != (uint8_t)(j & 0xFF)) { + free(rb_buffer); + return PWTEST_FAIL; + } + } + + free(rb_buffer); + + return PWTEST_PASS; +} + +/* + * Test: IEC61883 transmit PDU construction simulation. + * Builds PDU like setup_pdu() + flush_write() would, verifies structure. + */ +PWTEST(avb_iec61883_transmit_pdu) +{ + uint8_t pdu[2048]; + struct avb_frame_header *h; + struct avb_packet_iec61883 *p; + static const uint8_t dest[6] = { 0x91, 0xe0, 0xf0, 0x00, 0x01, 0x00 }; + static const uint8_t src[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + int channels = 8; + int stride = channels * 4; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; + int prio = 3; + int vlan_id = 2; + uint64_t stream_id = 0x020000fffe000001ULL; + + /* Simulate setup_pdu() */ + memset(pdu, 0, sizeof(pdu)); + h = (struct avb_frame_header *)pdu; + p = SPA_PTROFF(h, sizeof(*h), void); + + memcpy(h->dest, dest, 6); + memcpy(h->src, src, 6); + h->type = htons(0x8100); + h->prio_cfi_id = htons((prio << 13) | vlan_id); + h->etype = htons(0x22f0); + + p->subtype = AVB_SUBTYPE_61883_IIDC; + p->sv = 1; + p->stream_id = htobe64(stream_id); + p->data_len = htons(payload_size + 8); + p->tag = 0x1; + p->channel = 0x1f; + p->tcode = 0xa; + p->sid = 0x3f; + p->dbs = channels; + p->qi2 = 0x2; + p->format_id = 0x10; + p->fdf = 0x2; + p->syt = htons(0x0008); + + /* Simulate flush_write() per-PDU setup */ + p->seq_num = 0; + p->tv = 1; + p->timestamp = htonl(125000); + p->dbc = 0; + + /* Verify the PDU */ + pwtest_int_eq(p->subtype, AVB_SUBTYPE_61883_IIDC); + pwtest_int_eq(be64toh(p->stream_id), (int64_t)stream_id); + pwtest_int_eq(ntohs(p->data_len), payload_size + 8); + pwtest_int_eq(p->dbs, channels); + pwtest_int_eq(p->seq_num, 0); + pwtest_int_eq((int)ntohl(p->timestamp), 125000); + pwtest_int_eq(p->dbc, 0); + pwtest_int_eq(ntohs(h->etype), 0x22f0); + + /* Simulate second PDU — verify sequence and DBC advance */ + p->seq_num = 1; + p->timestamp = htonl(250000); + p->dbc = frames_per_pdu; + + pwtest_int_eq(p->seq_num, 1); + pwtest_int_eq(p->dbc, frames_per_pdu); + pwtest_int_eq((int)ntohl(p->timestamp), 250000); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer overrun detection. + * Simulates the overrun check in handle_iec61883_packet(). + */ +PWTEST(avb_ringbuffer_overrun) +{ + struct spa_ringbuffer ring; + uint8_t *buffer; + uint8_t data[256]; + uint32_t index; + int32_t filled; + int payload_size = 192; + int overrun_count = 0; + + buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(buffer); + spa_ringbuffer_init(&ring); + + memset(data, 0xAA, sizeof(data)); + + /* Fill the buffer to capacity */ + int max_writes = BUFFER_SIZE / payload_size; + for (int i = 0; i < max_writes; i++) { + filled = spa_ringbuffer_get_write_index(&ring, &index); + if (filled + payload_size > (int32_t)BUFFER_SIZE) { + overrun_count++; + break; + } + spa_ringbuffer_write_data(&ring, buffer, BUFFER_SIZE, + index % BUFFER_SIZE, data, payload_size); + index += payload_size; + spa_ringbuffer_write_update(&ring, index); + } + + /* Try one more write — should detect overrun */ + filled = spa_ringbuffer_get_write_index(&ring, &index); + if (filled + payload_size > (int32_t)BUFFER_SIZE) + overrun_count++; + + /* Should have hit at least one overrun */ + pwtest_int_gt(overrun_count, 0); + + /* Verify data still readable from the full buffer */ + filled = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_gt(filled, 0); + + free(buffer); + + return PWTEST_PASS; +} + +/* + * Test: Sequence number wrapping at 256 (uint8_t). + * Verifies that sequence numbers wrap correctly as in flush_write(). + */ +PWTEST(avb_sequence_number_wrapping) +{ + uint8_t seq = 0; + uint8_t dbc = 0; + int frames_per_pdu = 6; + + /* Simulate 300 PDU transmissions — seq wraps at 256 */ + for (int i = 0; i < 300; i++) { + pwtest_int_eq(seq, (uint8_t)(i & 0xFF)); + seq++; + dbc += frames_per_pdu; + } + + /* After 300 PDUs: seq = 300 & 0xFF = 44, dbc = 300*6 = 1800 & 0xFF = 8 */ + pwtest_int_eq(seq, (uint8_t)(300 & 0xFF)); + pwtest_int_eq(dbc, (uint8_t)(300 * frames_per_pdu)); + + return PWTEST_PASS; +} + +PWTEST_SUITE(avb) +{ + /* Phase 2: ADP and basic tests */ + pwtest_add(avb_adp_entity_available, PWTEST_NOARG); + pwtest_add(avb_adp_entity_departing, PWTEST_NOARG); + pwtest_add(avb_adp_entity_discover, PWTEST_NOARG); + pwtest_add(avb_adp_entity_timeout, PWTEST_NOARG); + pwtest_add(avb_mrp_attribute_lifecycle, PWTEST_NOARG); + pwtest_add(avb_milan_server_create, PWTEST_NOARG); + + /* Phase 3: MRP state machine tests */ + pwtest_add(avb_mrp_begin_join_new_tx, PWTEST_NOARG); + pwtest_add(avb_mrp_join_leave_cycle, PWTEST_NOARG); + pwtest_add(avb_mrp_rx_new_notification, PWTEST_NOARG); + pwtest_add(avb_mrp_registrar_leave_timer, PWTEST_NOARG); + pwtest_add(avb_mrp_multiple_attributes, PWTEST_NOARG); + + /* Phase 3: MSRP tests */ + pwtest_add(avb_msrp_attribute_types, PWTEST_NOARG); + pwtest_add(avb_msrp_domain_transmit, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_transmit, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_failed_notify, PWTEST_NOARG); + + /* Phase 3: MRP packet parsing tests */ + pwtest_add(avb_mrp_parse_single_domain, PWTEST_NOARG); + pwtest_add(avb_mrp_parse_with_lva, PWTEST_NOARG); + pwtest_add(avb_mrp_parse_three_values, PWTEST_NOARG); + + /* Phase 4: ACMP integration tests */ + pwtest_add(avb_acmp_not_supported, PWTEST_NOARG); + pwtest_add(avb_acmp_connect_tx_no_stream, PWTEST_NOARG); + pwtest_add(avb_acmp_wrong_entity_ignored, PWTEST_NOARG); + pwtest_add(avb_acmp_connect_rx_forward, PWTEST_NOARG); + pwtest_add(avb_acmp_pending_timeout, PWTEST_NOARG); + pwtest_add(avb_acmp_packet_filtering, PWTEST_NOARG); + + /* Phase 5: AECP/AEM entity model tests */ + pwtest_add(avb_aecp_read_descriptor_entity, PWTEST_NOARG); + pwtest_add(avb_aecp_read_descriptor_not_found, PWTEST_NOARG); + pwtest_add(avb_aecp_packet_filtering, PWTEST_NOARG); + pwtest_add(avb_aecp_unsupported_message_types, PWTEST_NOARG); + pwtest_add(avb_aecp_aem_not_implemented, PWTEST_NOARG); + pwtest_add(avb_aecp_acquire_entity_legacy, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_entity_legacy, PWTEST_NOARG); + pwtest_add(avb_aecp_entity_available_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_non_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_acquire_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_read_descriptor_milan, PWTEST_NOARG); + + /* Phase 7: Additional protocol coverage tests */ + pwtest_add(avb_maap_conflict_probe_in_announce, PWTEST_NOARG); + pwtest_add(avb_maap_defend_causes_reprobe, PWTEST_NOARG); + pwtest_add(avb_maap_announce_conflict, PWTEST_NOARG); + pwtest_add(avb_maap_no_conflict, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_rx_forward, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_tx_no_stream, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_pending_timeout, PWTEST_NOARG); + pwtest_add(avb_aecp_get_avb_info, PWTEST_NOARG); + pwtest_add(avb_aecp_get_avb_info_wrong_type, PWTEST_NOARG); + pwtest_add(avb_mrp_leave_all_timer, PWTEST_NOARG); + pwtest_add(avb_mrp_periodic_timer, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_failed_process, PWTEST_NOARG); + + /* Phase 8: MVRP/MMRP, ADP edge cases, descriptor, AECP command tests */ + pwtest_add(avb_mvrp_attribute_lifecycle, PWTEST_NOARG); + pwtest_add(avb_mvrp_vid_transmit, PWTEST_NOARG); + pwtest_add(avb_mmrp_attribute_types, PWTEST_NOARG); + pwtest_add(avb_adp_duplicate_entity_available, PWTEST_NOARG); + pwtest_add(avb_adp_targeted_discover, PWTEST_NOARG); + pwtest_add(avb_adp_readvertise_timing, PWTEST_NOARG); + pwtest_add(avb_adp_departure_before_timeout, PWTEST_NOARG); + pwtest_add(avb_descriptor_lookup_edge_cases, PWTEST_NOARG); + pwtest_add(avb_descriptor_data_integrity, PWTEST_NOARG); + pwtest_add(avb_aecp_get_configuration, PWTEST_NOARG); + pwtest_add(avb_aecp_get_sampling_rate, PWTEST_NOARG); + pwtest_add(avb_aecp_get_sampling_rate_wrong_type, PWTEST_NOARG); + pwtest_add(avb_aecp_get_name_entity, PWTEST_NOARG); + pwtest_add(avb_aecp_get_name_missing_descriptor, PWTEST_NOARG); + + /* Phase 6: AVTP audio data path tests */ + pwtest_add(avb_iec61883_packet_layout, PWTEST_NOARG); + pwtest_add(avb_aaf_packet_layout, PWTEST_NOARG); + pwtest_add(avb_frame_header_construction, PWTEST_NOARG); + pwtest_add(avb_pdu_size_calculations, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_audio_roundtrip, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_wraparound, PWTEST_NOARG); + pwtest_add(avb_iec61883_receive_simulation, PWTEST_NOARG); + pwtest_add(avb_iec61883_transmit_pdu, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_overrun, PWTEST_NOARG); + pwtest_add(avb_sequence_number_wrapping, PWTEST_NOARG); + + return PWTEST_PASS; +}