1175 lines
36 KiB
C++
1175 lines
36 KiB
C++
#include <warppipe/warppipe.hpp>
|
|
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
namespace {
|
|
|
|
warppipe::ConnectionOptions DefaultOptions() {
|
|
warppipe::ConnectionOptions options;
|
|
options.threading = warppipe::ThreadingMode::kThreadLoop;
|
|
options.autoconnect = true;
|
|
options.application_name = "warppipe-tests";
|
|
return options;
|
|
}
|
|
|
|
bool ContainsNode(const std::vector<warppipe::NodeInfo>& nodes, uint32_t id) {
|
|
for (const auto& node : nodes) {
|
|
if (node.id.value == id) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ContainsLink(const std::vector<warppipe::Link>& links, uint32_t id) {
|
|
for (const auto& link : links) {
|
|
if (link.id.value == id) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("caller thread mode is not implemented") {
|
|
warppipe::ConnectionOptions options = DefaultOptions();
|
|
options.threading = warppipe::ThreadingMode::kCallerThread;
|
|
auto result = warppipe::Client::Create(options);
|
|
REQUIRE_FALSE(result.ok());
|
|
REQUIRE(result.status.code == warppipe::StatusCode::kNotImplemented);
|
|
}
|
|
|
|
TEST_CASE("connects or reports unavailable") {
|
|
warppipe::ConnectionOptions connection_options = DefaultOptions();
|
|
auto result = warppipe::Client::Create(connection_options);
|
|
if (!result.ok()) {
|
|
REQUIRE(result.status.code == warppipe::StatusCode::kUnavailable);
|
|
return;
|
|
}
|
|
|
|
auto nodes = result.value->ListNodes();
|
|
REQUIRE(nodes.ok());
|
|
}
|
|
|
|
TEST_CASE("invalid remote name fails") {
|
|
warppipe::ConnectionOptions options = DefaultOptions();
|
|
options.remote_name = "warppipe-test-missing-remote";
|
|
auto result = warppipe::Client::Create(options);
|
|
REQUIRE_FALSE(result.ok());
|
|
REQUIRE(result.status.code == warppipe::StatusCode::kUnavailable);
|
|
}
|
|
|
|
TEST_CASE("create and remove virtual sink/source when available") {
|
|
warppipe::ConnectionOptions options = DefaultOptions();
|
|
auto result = warppipe::Client::Create(options);
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::VirtualNodeOptions node_options;
|
|
node_options.display_name = "warppipe-test-virtual";
|
|
node_options.group = "warppipe-test";
|
|
node_options.format.rate = 44100;
|
|
node_options.format.channels = 2;
|
|
auto sink = result.value->CreateVirtualSink("warppipe-test-sink", node_options);
|
|
if (!sink.ok()) {
|
|
if (sink.status.code == warppipe::StatusCode::kUnavailable) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
REQUIRE(sink.ok());
|
|
}
|
|
|
|
auto source = result.value->CreateVirtualSource("warppipe-test-source", node_options);
|
|
if (!source.ok()) {
|
|
if (source.status.code == warppipe::StatusCode::kUnavailable) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
REQUIRE(source.ok());
|
|
}
|
|
|
|
REQUIRE(result.value->RemoveNode(sink.value.node).ok());
|
|
REQUIRE(result.value->RemoveNode(source.value.node).ok());
|
|
}
|
|
|
|
TEST_CASE("missing media class returns invalid argument") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::VirtualNodeOptions options;
|
|
options.media_class_override = "";
|
|
auto sink = result.value->CreateVirtualSink("warppipe-test-missing-class", options);
|
|
REQUIRE_FALSE(sink.ok());
|
|
REQUIRE(sink.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("duplicate node name returns invalid argument") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto first = result.value->CreateVirtualSink("warppipe-dup", warppipe::VirtualNodeOptions{});
|
|
if (!first.ok()) {
|
|
if (first.status.code == warppipe::StatusCode::kUnavailable) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
REQUIRE(first.ok());
|
|
}
|
|
|
|
auto second = result.value->CreateVirtualSink("warppipe-dup", warppipe::VirtualNodeOptions{});
|
|
REQUIRE_FALSE(second.ok());
|
|
REQUIRE(second.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
|
|
REQUIRE(result.value->RemoveNode(first.value.node).ok());
|
|
}
|
|
|
|
TEST_CASE("loopback target missing returns not found") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::VirtualNodeOptions options;
|
|
options.behavior = warppipe::VirtualBehavior::kLoopback;
|
|
options.target_node = "warppipe-missing-target";
|
|
|
|
auto sink = result.value->CreateVirtualSink("warppipe-loopback", options);
|
|
REQUIRE_FALSE(sink.ok());
|
|
REQUIRE(sink.status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("registry removal cleans up ports and links") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
const uint32_t node_id = 500001;
|
|
const uint32_t out_port_id = 500002;
|
|
const uint32_t in_port_id = 500003;
|
|
const uint32_t link_id = 500004;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{node_id};
|
|
node.name = "warppipe-test-node";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
warppipe::PortInfo out_port;
|
|
out_port.id = warppipe::PortId{out_port_id};
|
|
out_port.node = warppipe::NodeId{node_id};
|
|
out_port.name = "output";
|
|
out_port.is_input = false;
|
|
REQUIRE(result.value->Test_InsertPort(out_port).ok());
|
|
|
|
warppipe::PortInfo in_port;
|
|
in_port.id = warppipe::PortId{in_port_id};
|
|
in_port.node = warppipe::NodeId{node_id};
|
|
in_port.name = "input";
|
|
in_port.is_input = true;
|
|
REQUIRE(result.value->Test_InsertPort(in_port).ok());
|
|
|
|
warppipe::Link link;
|
|
link.id = warppipe::LinkId{link_id};
|
|
link.output_port = warppipe::PortId{out_port_id};
|
|
link.input_port = warppipe::PortId{in_port_id};
|
|
REQUIRE(result.value->Test_InsertLink(link).ok());
|
|
|
|
auto snapshot = result.value->ListNodes();
|
|
REQUIRE(snapshot.ok());
|
|
REQUIRE(ContainsNode(snapshot.value, node_id));
|
|
|
|
REQUIRE(result.value->Test_RemoveGlobal(node_id).ok());
|
|
|
|
auto ports = result.value->ListPorts(warppipe::NodeId{node_id});
|
|
REQUIRE(ports.ok());
|
|
REQUIRE(ports.value.empty());
|
|
|
|
auto links = result.value->ListLinks();
|
|
REQUIRE(links.ok());
|
|
REQUIRE_FALSE(ContainsLink(links.value, link_id));
|
|
}
|
|
|
|
TEST_CASE("autoconnect reconnects after forced disconnect") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto nodes = result.value->ListNodes();
|
|
REQUIRE(nodes.ok());
|
|
|
|
REQUIRE(result.value->Test_ForceDisconnect().ok());
|
|
|
|
auto nodes_after = result.value->ListNodes();
|
|
REQUIRE(nodes_after.ok());
|
|
}
|
|
|
|
TEST_CASE("link creation validates ports and directions") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{600001};
|
|
node.name = "warppipe-link-node";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
warppipe::PortInfo out_port;
|
|
out_port.id = warppipe::PortId{600002};
|
|
out_port.node = node.id;
|
|
out_port.name = "out";
|
|
out_port.is_input = false;
|
|
REQUIRE(result.value->Test_InsertPort(out_port).ok());
|
|
|
|
warppipe::PortInfo in_port;
|
|
in_port.id = warppipe::PortId{600003};
|
|
in_port.node = node.id;
|
|
in_port.name = "in";
|
|
in_port.is_input = true;
|
|
REQUIRE(result.value->Test_InsertPort(in_port).ok());
|
|
|
|
auto invalid = result.value->CreateLink(warppipe::PortId{0}, in_port.id, warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(invalid.ok());
|
|
REQUIRE(invalid.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
|
|
auto missing_out = result.value->CreateLink(warppipe::PortId{123456}, in_port.id, warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(missing_out.ok());
|
|
REQUIRE(missing_out.status.code == warppipe::StatusCode::kNotFound);
|
|
|
|
auto missing_in = result.value->CreateLink(out_port.id, warppipe::PortId{123457}, warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(missing_in.ok());
|
|
REQUIRE(missing_in.status.code == warppipe::StatusCode::kNotFound);
|
|
|
|
auto mismatch = result.value->CreateLink(in_port.id, out_port.id, warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(mismatch.ok());
|
|
REQUIRE(mismatch.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("CreateLinkByName validates missing names") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto invalid = result.value->CreateLinkByName("", "out", "node", "in", warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(invalid.ok());
|
|
REQUIRE(invalid.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("duplicate links are rejected") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{600101};
|
|
node.name = "warppipe-link-dup";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
warppipe::PortInfo out_port;
|
|
out_port.id = warppipe::PortId{600102};
|
|
out_port.node = node.id;
|
|
out_port.name = "out";
|
|
out_port.is_input = false;
|
|
REQUIRE(result.value->Test_InsertPort(out_port).ok());
|
|
|
|
warppipe::PortInfo in_port;
|
|
in_port.id = warppipe::PortId{600103};
|
|
in_port.node = node.id;
|
|
in_port.name = "in";
|
|
in_port.is_input = true;
|
|
REQUIRE(result.value->Test_InsertPort(in_port).ok());
|
|
|
|
auto first = result.value->CreateLink(out_port.id, in_port.id, warppipe::LinkOptions{});
|
|
if (!first.ok()) {
|
|
SUCCEED("Link factory unavailable");
|
|
return;
|
|
}
|
|
|
|
auto second = result.value->CreateLink(out_port.id, in_port.id, warppipe::LinkOptions{});
|
|
REQUIRE_FALSE(second.ok());
|
|
REQUIRE(second.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("add route rule validates input") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule empty_match;
|
|
empty_match.target_node = "some-sink";
|
|
auto r1 = result.value->AddRouteRule(empty_match);
|
|
REQUIRE_FALSE(r1.ok());
|
|
REQUIRE(r1.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
|
|
warppipe::RouteRule empty_target;
|
|
empty_target.match.application_name = "firefox";
|
|
auto r2 = result.value->AddRouteRule(empty_target);
|
|
REQUIRE_FALSE(r2.ok());
|
|
REQUIRE(r2.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("add and remove route rules") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "firefox";
|
|
rule.target_node = "warppipe-test-sink";
|
|
|
|
auto add_result = result.value->AddRouteRule(rule);
|
|
REQUIRE(add_result.ok());
|
|
REQUIRE(add_result.value.value != 0);
|
|
|
|
auto list = result.value->ListRouteRules();
|
|
REQUIRE(list.ok());
|
|
REQUIRE(list.value.size() == 1);
|
|
REQUIRE(list.value[0].match.application_name == "firefox");
|
|
REQUIRE(list.value[0].target_node == "warppipe-test-sink");
|
|
|
|
REQUIRE(result.value->RemoveRouteRule(add_result.value).ok());
|
|
|
|
auto list2 = result.value->ListRouteRules();
|
|
REQUIRE(list2.ok());
|
|
REQUIRE(list2.value.empty());
|
|
}
|
|
|
|
TEST_CASE("remove nonexistent rule returns not found") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto status = result.value->RemoveRouteRule(warppipe::RuleId{99999});
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("policy engine creates pending auto-link for matching node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "test-app";
|
|
rule.target_node = "test-sink";
|
|
auto rule_result = result.value->AddRouteRule(rule);
|
|
REQUIRE(rule_result.ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
|
|
|
warppipe::NodeInfo source_node;
|
|
source_node.id = warppipe::NodeId{700001};
|
|
source_node.name = "test-source";
|
|
source_node.media_class = "Stream/Output/Audio";
|
|
source_node.application_name = "test-app";
|
|
REQUIRE(result.value->Test_InsertNode(source_node).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
|
}
|
|
|
|
TEST_CASE("policy engine ignores non-matching nodes") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "firefox";
|
|
rule.target_node = "test-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{700002};
|
|
node.name = "chromium-output";
|
|
node.media_class = "Stream/Output/Audio";
|
|
node.application_name = "chromium";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
|
}
|
|
|
|
TEST_CASE("existing rules match when rule is added after node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{700003};
|
|
node.name = "existing-source";
|
|
node.media_class = "Stream/Output/Audio";
|
|
node.application_name = "test-app";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "test-app";
|
|
rule.target_node = "test-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
|
}
|
|
|
|
TEST_CASE("app disappear and reappear re-triggers policy") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "ephemeral-app";
|
|
rule.target_node = "test-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{700010};
|
|
node.name = "ephemeral-output";
|
|
node.media_class = "Stream/Output/Audio";
|
|
node.application_name = "ephemeral-app";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
|
|
|
REQUIRE(result.value->Test_RemoveGlobal(700010).ok());
|
|
|
|
warppipe::NodeInfo node2;
|
|
node2.id = warppipe::NodeId{700011};
|
|
node2.name = "ephemeral-output-2";
|
|
node2.media_class = "Stream/Output/Audio";
|
|
node2.application_name = "ephemeral-app";
|
|
REQUIRE(result.value->Test_InsertNode(node2).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() >= 1);
|
|
}
|
|
|
|
TEST_CASE("conflicting rules resolved deterministically") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule1;
|
|
rule1.match.application_name = "multi-match-app";
|
|
rule1.target_node = "sink-a";
|
|
auto r1 = result.value->AddRouteRule(rule1);
|
|
REQUIRE(r1.ok());
|
|
|
|
warppipe::RouteRule rule2;
|
|
rule2.match.application_name = "multi-match-app";
|
|
rule2.target_node = "sink-b";
|
|
auto r2 = result.value->AddRouteRule(rule2);
|
|
REQUIRE(r2.ok());
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{700020};
|
|
node.name = "multi-match-output";
|
|
node.media_class = "Stream/Output/Audio";
|
|
node.application_name = "multi-match-app";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 2);
|
|
}
|
|
|
|
TEST_CASE("save and load config round trip") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "firefox";
|
|
rule.match.media_role = "Music";
|
|
rule.target_node = "headphones";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
const char* path = "/tmp/warppipe_test_config.json";
|
|
REQUIRE(result.value->SaveConfig(path).ok());
|
|
|
|
auto result2 = warppipe::Client::Create(DefaultOptions());
|
|
if (!result2.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
REQUIRE(result2.value->LoadConfig(path).ok());
|
|
|
|
auto rules = result2.value->ListRouteRules();
|
|
REQUIRE(rules.ok());
|
|
REQUIRE(rules.value.size() == 1);
|
|
REQUIRE(rules.value[0].match.application_name == "firefox");
|
|
REQUIRE(rules.value[0].match.media_role == "Music");
|
|
REQUIRE(rules.value[0].target_node == "headphones");
|
|
|
|
std::remove(path);
|
|
}
|
|
|
|
TEST_CASE("load corrupted config returns error") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
const char* path = "/tmp/warppipe_test_corrupt.json";
|
|
{
|
|
std::ofstream f(path);
|
|
f << "{{{{not valid json!!!!";
|
|
}
|
|
|
|
auto status = result.value->LoadConfig(path);
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kInvalidArgument);
|
|
|
|
std::remove(path);
|
|
}
|
|
|
|
TEST_CASE("load missing config returns not found") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto status = result.value->LoadConfig("/tmp/warppipe_nonexistent_config_12345.json");
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("save config with empty path returns error") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto status = result.value->SaveConfig("");
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("load config missing version returns error") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
const char* path = "/tmp/warppipe_test_noversion.json";
|
|
{
|
|
std::ofstream f(path);
|
|
f << R"({"route_rules": []})";
|
|
}
|
|
|
|
auto status = result.value->LoadConfig(path);
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kInvalidArgument);
|
|
|
|
std::remove(path);
|
|
}
|
|
|
|
TEST_CASE("metadata defaults are initially empty") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto defaults = result.value->GetDefaults();
|
|
REQUIRE(defaults.ok());
|
|
}
|
|
|
|
TEST_CASE("set default sink without metadata returns unavailable") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto status = result.value->SetDefaultSink("");
|
|
REQUIRE_FALSE(status.ok());
|
|
}
|
|
|
|
TEST_CASE("set default source without metadata returns unavailable") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto status = result.value->SetDefaultSource("");
|
|
REQUIRE_FALSE(status.ok());
|
|
}
|
|
|
|
TEST_CASE("GetVirtualNodeInfo returns details for created virtual sink") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::VirtualNodeOptions options;
|
|
options.display_name = "warppipe-test-info";
|
|
options.group = "warppipe-test";
|
|
options.format.rate = 48000;
|
|
options.format.channels = 2;
|
|
|
|
auto sink = result.value->CreateVirtualSink("warppipe-info-sink", options);
|
|
if (!sink.ok()) {
|
|
if (sink.status.code == warppipe::StatusCode::kUnavailable) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
REQUIRE(sink.ok());
|
|
}
|
|
|
|
auto info = result.value->GetVirtualNodeInfo(sink.value.node);
|
|
REQUIRE(info.ok());
|
|
REQUIRE(info.value.node.value == sink.value.node.value);
|
|
REQUIRE(info.value.name == sink.value.name);
|
|
REQUIRE_FALSE(info.value.is_source);
|
|
|
|
REQUIRE(result.value->RemoveNode(sink.value.node).ok());
|
|
}
|
|
|
|
TEST_CASE("GetVirtualNodeInfo missing node returns not found") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto info = result.value->GetVirtualNodeInfo(warppipe::NodeId{999999});
|
|
REQUIRE_FALSE(info.ok());
|
|
REQUIRE(info.status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("NodeInfo captures application properties") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{800001};
|
|
node.name = "test-node-props";
|
|
node.media_class = "Audio/Sink";
|
|
node.application_name = "my-app";
|
|
node.process_binary = "my-binary";
|
|
node.media_role = "Music";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
auto nodes = result.value->ListNodes();
|
|
REQUIRE(nodes.ok());
|
|
for (const auto& n : nodes.value) {
|
|
if (n.id.value == 800001) {
|
|
REQUIRE(n.application_name == "my-app");
|
|
REQUIRE(n.process_binary == "my-binary");
|
|
REQUIRE(n.media_role == "Music");
|
|
return;
|
|
}
|
|
}
|
|
FAIL("inserted node not found");
|
|
}
|
|
|
|
TEST_CASE("policy-only mode does not create auto-links") {
|
|
warppipe::ConnectionOptions opts = DefaultOptions();
|
|
opts.policy_only = true;
|
|
auto result = warppipe::Client::Create(opts);
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "policy-only-app";
|
|
rule.target_node = "policy-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
warppipe::NodeInfo sink;
|
|
sink.id = warppipe::NodeId{900001};
|
|
sink.name = "policy-sink";
|
|
sink.media_class = "Audio/Sink";
|
|
REQUIRE(result.value->Test_InsertNode(sink).ok());
|
|
|
|
warppipe::PortInfo sink_port;
|
|
sink_port.id = warppipe::PortId{900002};
|
|
sink_port.node = sink.id;
|
|
sink_port.name = "playback_FL";
|
|
sink_port.is_input = true;
|
|
REQUIRE(result.value->Test_InsertPort(sink_port).ok());
|
|
|
|
warppipe::NodeInfo source;
|
|
source.id = warppipe::NodeId{900003};
|
|
source.name = "policy-only-source";
|
|
source.media_class = "Stream/Output/Audio";
|
|
source.application_name = "policy-only-app";
|
|
REQUIRE(result.value->Test_InsertNode(source).ok());
|
|
|
|
warppipe::PortInfo src_port;
|
|
src_port.id = warppipe::PortId{900004};
|
|
src_port.node = source.id;
|
|
src_port.name = "capture_FL";
|
|
src_port.is_input = false;
|
|
REQUIRE(result.value->Test_InsertPort(src_port).ok());
|
|
|
|
REQUIRE(result.value->Test_TriggerPolicyCheck().ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
|
|
|
auto links = result.value->ListLinks();
|
|
REQUIRE(links.ok());
|
|
bool found_auto_link = false;
|
|
for (const auto& link : links.value) {
|
|
if (link.output_port.value == 900004 && link.input_port.value == 900002) {
|
|
found_auto_link = true;
|
|
}
|
|
}
|
|
REQUIRE_FALSE(found_auto_link);
|
|
}
|
|
|
|
TEST_CASE("policy engine does not re-create existing links") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo sink;
|
|
sink.id = warppipe::NodeId{910001};
|
|
sink.name = "relink-sink";
|
|
sink.media_class = "Audio/Sink";
|
|
REQUIRE(result.value->Test_InsertNode(sink).ok());
|
|
|
|
warppipe::PortInfo sink_port;
|
|
sink_port.id = warppipe::PortId{910002};
|
|
sink_port.node = sink.id;
|
|
sink_port.name = "playback_FL";
|
|
sink_port.is_input = true;
|
|
REQUIRE(result.value->Test_InsertPort(sink_port).ok());
|
|
|
|
warppipe::NodeInfo source;
|
|
source.id = warppipe::NodeId{910003};
|
|
source.name = "relink-source";
|
|
source.media_class = "Stream/Output/Audio";
|
|
source.application_name = "relink-app";
|
|
REQUIRE(result.value->Test_InsertNode(source).ok());
|
|
|
|
warppipe::PortInfo src_port;
|
|
src_port.id = warppipe::PortId{910004};
|
|
src_port.node = source.id;
|
|
src_port.name = "capture_FL";
|
|
src_port.is_input = false;
|
|
REQUIRE(result.value->Test_InsertPort(src_port).ok());
|
|
|
|
warppipe::Link existing_link;
|
|
existing_link.id = warppipe::LinkId{910005};
|
|
existing_link.output_port = warppipe::PortId{910004};
|
|
existing_link.input_port = warppipe::PortId{910002};
|
|
REQUIRE(result.value->Test_InsertLink(existing_link).ok());
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "relink-app";
|
|
rule.target_node = "relink-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
REQUIRE(result.value->Test_TriggerPolicyCheck().ok());
|
|
|
|
auto links = result.value->ListLinks();
|
|
REQUIRE(links.ok());
|
|
int matching_links = 0;
|
|
for (const auto& link : links.value) {
|
|
if (link.output_port.value == 910004 && link.input_port.value == 910002) {
|
|
++matching_links;
|
|
}
|
|
}
|
|
REQUIRE(matching_links == 1);
|
|
}
|
|
|
|
TEST_CASE("policy mode does not override user defaults") {
|
|
warppipe::ConnectionOptions opts = DefaultOptions();
|
|
opts.policy_only = true;
|
|
auto result = warppipe::Client::Create(opts);
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
auto defaults = result.value->GetDefaults();
|
|
REQUIRE(defaults.ok());
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "some-app";
|
|
rule.target_node = "custom-sink";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
auto defaults2 = result.value->GetDefaults();
|
|
REQUIRE(defaults2.ok());
|
|
REQUIRE(defaults2.value.default_sink_name == defaults.value.default_sink_name);
|
|
REQUIRE(defaults2.value.default_source_name == defaults.value.default_source_name);
|
|
REQUIRE(defaults2.value.configured_sink_name == defaults.value.configured_sink_name);
|
|
REQUIRE(defaults2.value.configured_source_name == defaults.value.configured_source_name);
|
|
}
|
|
|
|
TEST_CASE("Test_SetNodeVolume sets and retrieves volume state") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{900};
|
|
node.name = "vol-sink";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
|
|
auto vol = client->Test_GetNodeVolume(warppipe::NodeId{900});
|
|
REQUIRE(vol.ok());
|
|
REQUIRE(vol.value.volume == Catch::Approx(1.0f));
|
|
REQUIRE(vol.value.mute == false);
|
|
|
|
REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{900}, 0.5f, false).ok());
|
|
vol = client->Test_GetNodeVolume(warppipe::NodeId{900});
|
|
REQUIRE(vol.ok());
|
|
REQUIRE(vol.value.volume == Catch::Approx(0.5f));
|
|
REQUIRE(vol.value.mute == false);
|
|
|
|
REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{900}, 0.75f, true).ok());
|
|
vol = client->Test_GetNodeVolume(warppipe::NodeId{900});
|
|
REQUIRE(vol.ok());
|
|
REQUIRE(vol.value.volume == Catch::Approx(0.75f));
|
|
REQUIRE(vol.value.mute == true);
|
|
}
|
|
|
|
TEST_CASE("Test_SetNodeVolume clamps volume") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{901};
|
|
node.name = "vol-clamp";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{901}, 2.0f, false).ok());
|
|
auto vol = client->Test_GetNodeVolume(warppipe::NodeId{901});
|
|
REQUIRE(vol.ok());
|
|
REQUIRE(vol.value.volume == Catch::Approx(1.5f));
|
|
|
|
REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{901}, -1.0f, false).ok());
|
|
vol = client->Test_GetNodeVolume(warppipe::NodeId{901});
|
|
REQUIRE(vol.ok());
|
|
REQUIRE(vol.value.volume == Catch::Approx(0.0f));
|
|
}
|
|
|
|
TEST_CASE("Test_SetNodeVolume fails for nonexistent node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
|
|
auto status = result.value->Test_SetNodeVolume(warppipe::NodeId{999}, 0.5f, false);
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("EnsureNodeMeter and NodeMeterPeak round-trip") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{950};
|
|
node.name = "meter-test";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{950}).ok());
|
|
|
|
auto peak = client->NodeMeterPeak(warppipe::NodeId{950});
|
|
REQUIRE(peak.ok());
|
|
REQUIRE(peak.value.peak_left == Catch::Approx(0.0f));
|
|
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
|
}
|
|
|
|
TEST_CASE("Test_SetNodeMeterPeak updates peaks") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{951};
|
|
node.name = "meter-set";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{951}, 0.6f, 0.8f).ok());
|
|
|
|
auto peak = client->NodeMeterPeak(warppipe::NodeId{951});
|
|
REQUIRE(peak.ok());
|
|
REQUIRE(peak.value.peak_left == Catch::Approx(0.6f));
|
|
REQUIRE(peak.value.peak_right == Catch::Approx(0.8f));
|
|
}
|
|
|
|
TEST_CASE("DisableNodeMeter removes metering") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{952};
|
|
node.name = "meter-disable";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{952}).ok());
|
|
REQUIRE(client->DisableNodeMeter(warppipe::NodeId{952}).ok());
|
|
|
|
auto peak = client->NodeMeterPeak(warppipe::NodeId{952});
|
|
REQUIRE_FALSE(peak.ok());
|
|
REQUIRE(peak.status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("MasterMeterPeak defaults to zero") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
|
|
auto peak = result.value->MeterPeak();
|
|
REQUIRE(peak.ok());
|
|
REQUIRE(peak.value.peak_left == Catch::Approx(0.0f));
|
|
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
|
}
|
|
|
|
TEST_CASE("Test_SetMasterMeterPeak updates master peaks") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
|
|
REQUIRE(result.value->Test_SetMasterMeterPeak(0.9f, 0.7f).ok());
|
|
|
|
auto peak = result.value->MeterPeak();
|
|
REQUIRE(peak.ok());
|
|
REQUIRE(peak.value.peak_left == Catch::Approx(0.9f));
|
|
REQUIRE(peak.value.peak_right == Catch::Approx(0.7f));
|
|
}
|
|
|
|
TEST_CASE("Test_SetNodeMeterPeak clamps values") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
auto &client = result.value;
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{953};
|
|
node.name = "meter-clamp";
|
|
node.media_class = "Audio/Sink";
|
|
REQUIRE(client->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{953}, 1.5f, -0.5f).ok());
|
|
|
|
auto peak = client->NodeMeterPeak(warppipe::NodeId{953});
|
|
REQUIRE(peak.ok());
|
|
REQUIRE(peak.value.peak_left == Catch::Approx(1.0f));
|
|
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
|
}
|
|
|
|
TEST_CASE("EnsureNodeMeter fails for nonexistent node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
REQUIRE(result.ok());
|
|
|
|
auto status = result.value->EnsureNodeMeter(warppipe::NodeId{999});
|
|
REQUIRE_FALSE(status.ok());
|
|
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
|
}
|
|
|
|
TEST_CASE("capture rule validation rejects empty source_node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "discord";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
auto r = result.value->AddRouteRule(rule);
|
|
REQUIRE_FALSE(r.ok());
|
|
REQUIRE(r.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("capture rule validation accepts valid source_node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "discord";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
rule.source_node = "alsa_input.usb-mic";
|
|
auto r = result.value->AddRouteRule(rule);
|
|
REQUIRE(r.ok());
|
|
REQUIRE(r.value.value != 0);
|
|
|
|
auto list = result.value->ListRouteRules();
|
|
REQUIRE(list.ok());
|
|
REQUIRE(list.value.size() == 1);
|
|
REQUIRE(list.value[0].direction == warppipe::RuleDirection::kCapture);
|
|
REQUIRE(list.value[0].source_node == "alsa_input.usb-mic");
|
|
}
|
|
|
|
TEST_CASE("capture rule creates pending auto-link for matching app") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "teams";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
rule.source_node = "hw-mic";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
|
|
|
warppipe::NodeInfo app_node;
|
|
app_node.id = warppipe::NodeId{800001};
|
|
app_node.name = "teams-capture";
|
|
app_node.media_class = "Stream/Input/Audio";
|
|
app_node.application_name = "teams";
|
|
REQUIRE(result.value->Test_InsertNode(app_node).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
|
}
|
|
|
|
TEST_CASE("capture rule ignores non-matching app") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "discord";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
rule.source_node = "hw-mic";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{800002};
|
|
node.name = "zoom-capture";
|
|
node.media_class = "Stream/Input/Audio";
|
|
node.application_name = "zoom";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
|
}
|
|
|
|
TEST_CASE("capture rule added after app creates pending link") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::NodeInfo node;
|
|
node.id = warppipe::NodeId{800003};
|
|
node.name = "discord-capture";
|
|
node.media_class = "Stream/Input/Audio";
|
|
node.application_name = "discord";
|
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "discord";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
rule.source_node = "hw-mic";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
|
}
|
|
|
|
TEST_CASE("playback rule still rejects empty target_node") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "firefox";
|
|
rule.direction = warppipe::RuleDirection::kPlayback;
|
|
auto r = result.value->AddRouteRule(rule);
|
|
REQUIRE_FALSE(r.ok());
|
|
REQUIRE(r.status.code == warppipe::StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST_CASE("save and load capture rule round trip") {
|
|
auto result = warppipe::Client::Create(DefaultOptions());
|
|
if (!result.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
warppipe::RouteRule rule;
|
|
rule.match.application_name = "discord";
|
|
rule.match.process_binary = "Discord";
|
|
rule.direction = warppipe::RuleDirection::kCapture;
|
|
rule.source_node = "alsa_input.usb-mic";
|
|
REQUIRE(result.value->AddRouteRule(rule).ok());
|
|
|
|
const char* path = "/tmp/warppipe_test_capture_config.json";
|
|
REQUIRE(result.value->SaveConfig(path).ok());
|
|
|
|
auto result2 = warppipe::Client::Create(DefaultOptions());
|
|
if (!result2.ok()) {
|
|
SUCCEED("PipeWire unavailable");
|
|
return;
|
|
}
|
|
|
|
REQUIRE(result2.value->LoadConfig(path).ok());
|
|
|
|
auto rules = result2.value->ListRouteRules();
|
|
REQUIRE(rules.ok());
|
|
REQUIRE(rules.value.size() == 1);
|
|
REQUIRE(rules.value[0].match.application_name == "discord");
|
|
REQUIRE(rules.value[0].match.process_binary == "Discord");
|
|
REQUIRE(rules.value[0].direction == warppipe::RuleDirection::kCapture);
|
|
REQUIRE(rules.value[0].source_node == "alsa_input.usb-mic");
|
|
REQUIRE(rules.value[0].target_node.empty());
|
|
|
|
std::remove(path);
|
|
}
|