#include #include #include #include #include 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& nodes, uint32_t id) { for (const auto& node : nodes) { if (node.id.value == id) { return true; } } return false; } bool ContainsLink(const std::vector& 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("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"); }