Files
hakurei/internal/system/pipewire_test.go
Ophestra a6600be34a
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m12s
Test / ShareFS (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 6m44s
Test / Flake checks (push) Successful in 1m24s
all: use filepath
This makes package check portable, and removes nonportable behaviour from package pkg, pipewire, and system. All other packages remain nonportable due to their nature. No latency increase was observed due to this change on amd64 and arm64 linux.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 18:24:53 +09:00

553 lines
16 KiB
Go

package system
import (
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"hakurei.app/internal/acl"
"hakurei.app/internal/pipewire"
"hakurei.app/internal/stub"
)
func TestPipeWireOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
{"success", 0xbeef, 0xff, &pipewireOp{nil,
m(filepath.Join(t.TempDir(), "pipewire")),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, []stub.Call{
call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context {
if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{
/* roundtrip 0 */
string([]byte{
// header: Core::Hello
0, 0, 0, 0,
0x18, 0, 0, 1,
0, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x10, 0, 0, 0,
0xe, 0, 0, 0,
// Int: version = 4
4, 0, 0, 0,
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
// header: Client::UpdateProperties
1, 0, 0, 0,
0x50, 0, 0, 2,
1, 0, 0, 0,
0, 0, 0, 0,
// Struct: spa_dict
0x48, 0, 0, 0,
0xe, 0, 0, 0,
// Struct: spa_dict_item
0x40, 0, 0, 0,
0xe, 0, 0, 0,
// Int: n_items
4, 0, 0, 0,
4, 0, 0, 0,
1, 0, 0, 0,
0, 0, 0, 0,
// String: key = "remote.intention"
0x11, 0, 0, 0,
8, 0, 0, 0,
0x72, 0x65, 0x6d, 0x6f,
0x74, 0x65, 0x2e, 0x69,
0x6e, 0x74, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e,
0, 0, 0, 0,
0, 0, 0, 0,
// String: value = "manager"
8, 0, 0, 0,
8, 0, 0, 0,
0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x72, 0,
// header: Core::GetRegistry
0, 0, 0, 0,
0x28, 0, 0, 5,
2, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: version = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// Int: new_id = 2
4, 0, 0, 0,
4, 0, 0, 0,
2, 0, 0, 0,
0, 0, 0, 0,
// header: Core::Sync
0, 0, 0, 0,
0x28, 0, 0, 2,
3, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 0
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Int: seq = 0x40000003
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0x40,
0, 0, 0, 0,
}),
/* roundtrip 1 */
string([]byte{
// header: Registry::Bind
2, 0, 0, 0,
0x68, 0, 0, 1,
4, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x60, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// String: type = "PipeWire:Interface:SecurityContext"
0x23, 0, 0, 0,
8, 0, 0, 0,
0x50, 0x69, 0x70, 0x65,
0x57, 0x69, 0x72, 0x65,
0x3a, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x66, 0x61,
0x63, 0x65, 0x3a, 0x53,
0x65, 0x63, 0x75, 0x72,
0x69, 0x74, 0x79, 0x43,
0x6f, 0x6e, 0x74, 0x65,
0x78, 0x74, 0, 0,
0, 0, 0, 0,
// Int: version = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// Int: new_id = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
}),
/* roundtrip 2 */
string([]byte{
// header: SecurityContext::Create
3, 0, 0, 0,
0x40, 1, 0, 1,
5, 0, 0, 0,
2, 0, 0, 0,
// Struct
0x38, 1, 0, 0,
0xe, 0, 0, 0,
// Fd: listen_fd = 1
8, 0, 0, 0,
0x12, 0, 0, 0,
1, 0, 0, 0,
0, 0, 0, 0,
// Fd: close_fd = 0
8, 0, 0, 0,
0x12, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Struct: spa_dict
0x10, 1, 0, 0,
0xe, 0, 0, 0,
// Int: n_items = 4
4, 0, 0, 0,
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
// String: key = "pipewire.sec.engine"
0x14, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x65, 0x6e, 0x67,
0x69, 0x6e, 0x65, 0,
0, 0, 0, 0,
// String: value = "app.hakurei"
0xc, 0, 0, 0,
8, 0, 0, 0,
0x61, 0x70, 0x70, 0x2e,
0x68, 0x61, 0x6b, 0x75,
0x72, 0x65, 0x69, 0,
0, 0, 0, 0,
// String: key = "pipewire.sec.app-id"
0x14, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x61, 0x70, 0x70,
0x2d, 0x69, 0x64, 0,
0, 0, 0, 0,
// String: value = "org.chromium.Chromium"
0x16, 0, 0, 0,
8, 0, 0, 0,
0x6f, 0x72, 0x67, 0x2e,
0x63, 0x68, 0x72, 0x6f,
0x6d, 0x69, 0x75, 0x6d,
0x2e, 0x43, 0x68, 0x72,
0x6f, 0x6d, 0x69, 0x75,
// String: key = "pipewire.sec.instance-id"
0x6d, 0, 0, 0,
0x19, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x69, 0x6e, 0x73,
0x74, 0x61, 0x6e, 0x63,
0x65, 0x2d, 0x69, 0x64,
0, 0, 0, 0,
0, 0, 0, 0,
// String: value = "ebf083d1b175911782d413369b64ce7c"
0x21, 0, 0, 0,
8, 0, 0, 0,
0x65, 0x62, 0x66, 0x30,
0x38, 0x33, 0x64, 0x31,
0x62, 0x31, 0x37, 0x35,
0x39, 0x31, 0x31, 0x37,
0x38, 0x32, 0x64, 0x34,
0x31, 0x33, 0x33, 0x36,
0x39, 0x62, 0x36, 0x34,
0x63, 0x65, 0x37, 0x63,
0, 0, 0, 0,
0, 0, 0, 0,
// String: key = "pipewire.access"
0x10, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x61, 0x63, 0x63,
0x65, 0x73, 0x73, 0,
// String: value = "restricted"
0xb, 0, 0, 0,
8, 0, 0, 0,
0x72, 0x65, 0x73, 0x74,
0x72, 0x69, 0x63, 0x74,
0x65, 0x64, 0, 0,
0, 0, 0, 0,
// header: Core::Sync
0, 0, 0, 0,
0x28, 0, 0, 2,
6, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 0
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Int: seq = 0x40000006
4, 0, 0, 0,
4, 0, 0, 0,
6, 0, 0, 0x40,
0, 0, 0, 0,
}),
}, recvmsg: []*stubMessage{
/* roundtrip 0 */
{pipewire.PW_ID_CORE, nil, &pipewire.CoreInfo{
ID: pipewire.PW_ID_CORE,
Cookie: -2069267610,
UserName: "alice",
HostName: "nixos",
Version: "1.4.7",
Name: "pipewire-0",
ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"},
{Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"},
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"},
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"},
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
{Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"},
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
},
}},
{pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{
ID: pipewire.PW_ID_CLIENT,
GlobalID: 34,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
},
}},
{pipewire.PW_ID_CLIENT, nil, &pipewire.ClientInfo{
ID: 34,
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
},
}},
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: -1,
Sequence: 0,
}},
{2, nil, &pipewire.RegistryGlobal{
ID: pipewire.PW_ID_CORE,
Permissions: pipewire.PW_CORE_PERM_MASK,
Type: pipewire.PW_TYPE_INTERFACE_Core,
Version: pipewire.PW_VERSION_CORE,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
},
}},
{2, nil, &pipewire.RegistryGlobal{
ID: 1,
Permissions: pipewire.PW_MODULE_PERM_MASK,
Type: pipewire.PW_TYPE_INTERFACE_Module,
Version: pipewire.PW_VERSION_MODULE,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"},
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
},
}},
{2, nil, &pipewire.RegistryGlobal{
ID: 3,
Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
},
}},
{2, nil, &pipewire.RegistryGlobal{
ID: 2,
Permissions: pipewire.PW_MODULE_PERM_MASK,
Type: pipewire.PW_TYPE_INTERFACE_Module,
Version: pipewire.PW_VERSION_MODULE,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"},
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
},
}},
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: 0,
Sequence: pipewire.CoreSyncSequenceOffset + 3,
}},
{2, nil, &pipewire.RegistryGlobal{
ID: 4,
Permissions: pipewire.PW_CLIENT_PERM_MASK,
Type: pipewire.PW_TYPE_INTERFACE_Client,
Version: pipewire.PW_VERSION_CLIENT,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"},
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
{Key: pipewire.PW_KEY_SEC_PID, Value: "1447"},
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
{Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"},
},
}},
nil,
/* roundtrip 1 */
{pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{
ID: 3,
GlobalID: 3,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
},
}},
nil,
/* roundtrip 2 */
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: 0,
Sequence: pipewire.CoreSyncSequenceOffset + 6,
}},
nil,
}}, pipewire.SPADict{
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
}); err != nil {
panic(err)
} else {
return ctx
}
}, nil),
call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil),
call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil),
call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil),
}, nil},
})
checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{
{"sample", 0xcafe, func(_ *testing.T, sys *I) {
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c")
}, []Op{&pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}}, stub.Expect{}},
})
checkOpIs(t, []opIsTestCase{
{"dst differs", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, false},
{"equals", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"sample", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire",
`pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`},
})
}
// stubMessage is a [pipewire.Message] prepared ahead of time.
type stubMessage struct {
// Proxy Id included in the [pipewire.Header].
id pipewire.Int
// Footer optionally appended after the message body.
footer pipewire.KnownSize
// Known-good message prepared ahead of time.
m pipewire.Message
}
// stubPipeWireConn implements [pipewire.Conn] and checks the behaviour of [pipewire.Context].
type stubPipeWireConn struct {
// Marshaled and sent for Recvmsg.
recvmsg []*stubMessage
// Current position in recvmsg.
curRecvmsg int
// Current server seq number.
sequence pipewire.Int
// Compared against calls to Sendmsg.
sendmsg []string
// Current position in sendmsg.
curSendmsg int
}
func (conn *stubPipeWireConn) MightBlock(timeout time.Duration) {
if timeout != pipewire.RoundtripTimeout {
panic("unexpected timeout " + timeout.String())
}
}
// Recvmsg marshals and copies a stubMessage prepared ahead of time.
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
defer func() { conn.curRecvmsg++ }()
recvflags = syscall.MSG_CMSG_CLOEXEC
if conn.recvmsg[conn.curRecvmsg] == nil {
err = syscall.EAGAIN
return
}
defer func() { conn.sequence++ }()
if data, marshalErr := (pipewire.MessageEncoder{Message: conn.recvmsg[conn.curRecvmsg].m}).AppendMessage(nil,
conn.recvmsg[conn.curRecvmsg].id,
conn.sequence,
conn.recvmsg[conn.curRecvmsg].footer,
); marshalErr != nil {
panic(marshalErr)
} else {
n = copy(p, data)
}
return
}
// Sendmsg checks a client message against a known-good sample.
func (conn *stubPipeWireConn) Sendmsg(p, _ []byte, _ int) (n int, err error) {
defer func() { conn.curSendmsg++ }()
n = len(p)
if string(p) != conn.sendmsg[conn.curSendmsg] {
err = fmt.Errorf("%#v, want %#v", p, []byte(conn.sendmsg[conn.curSendmsg]))
}
return
}
// Close checks whether Recvmsg and Sendmsg has depleted all samples.
func (conn *stubPipeWireConn) Close() error {
if conn.curRecvmsg != len(conn.recvmsg) {
return fmt.Errorf("consumed %d recvmsg samples, want %d", conn.curRecvmsg, len(conn.recvmsg))
}
if conn.curSendmsg != len(conn.sendmsg) {
return fmt.Errorf("consumed %d sendmsg samples, want %d", conn.curSendmsg, len(conn.sendmsg))
}
return nil
}