Milestone 4
This commit is contained in:
parent
691eb327d0
commit
420da2d468
6 changed files with 1039 additions and 24 deletions
|
|
@ -1,5 +1,9 @@
|
|||
#include <warppipe/warppipe.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
namespace {
|
||||
|
|
@ -310,3 +314,342 @@ TEST_CASE("duplicate links are rejected") {
|
|||
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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue