mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-04-09 08:21:08 -04:00
test: add AVB protocol test suite with loopback transport
Add a test suite for the AVB (Audio Video Bridging) protocol stack that runs entirely in software, requiring no hardware, root privileges, or running PipeWire daemon. The loopback transport (avb-transport-loopback.h) replaces raw AF_PACKET sockets with in-memory packet capture, using a synthetic MAC address and eventfd for protocol handlers that need a valid fd. Test utilities (test-avb-utils.h) provide helpers for creating test servers, injecting packets, advancing time, and building ADP packets. Tests cover: - ADP entity available/departing/discover/timeout - MRP attribute lifecycle (create, begin, join) - Milan v1.2 mode server creation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a73988d38d
commit
ef4ff8cfd0
4 changed files with 730 additions and 0 deletions
149
src/modules/module-avb/avb-transport-loopback.h
Normal file
149
src/modules/module-avb/avb-transport-loopback.h
Normal file
|
|
@ -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 <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
|
||||||
|
#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 */
|
||||||
|
|
@ -163,3 +163,42 @@ if valgrind.found()
|
||||||
env : valgrind_env,
|
env : valgrind_env,
|
||||||
timeout_multiplier : 3)
|
timeout_multiplier : 3)
|
||||||
endif
|
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
|
||||||
|
|
|
||||||
230
test/test-avb-utils.h
Normal file
230
test/test-avb-utils.h
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
/* AVB test utilities */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#ifndef TEST_AVB_UTILS_H
|
||||||
|
#define TEST_AVB_UTILS_H
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
|
||||||
|
#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-descriptors.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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* TEST_AVB_UTILS_H */
|
||||||
312
test/test-avb.c
Normal file
312
test/test-avb.c
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
/* AVB tests */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#include "pwtest.h"
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
|
||||||
|
#include "module-avb/aecp-aem-descriptors.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
PWTEST_SUITE(avb)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
return PWTEST_PASS;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue