Skip to content

Commit e964fa7

Browse files
Make Auto Optimizer policy tests hardware-independent
1 parent 5eb0a0e commit e964fa7

8 files changed

Lines changed: 362 additions & 393 deletions

File tree

obs-studio-client/tests/autoconfig-client-contract-test.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,48 @@ TEST_CASE("AutoConfig client projects ordered progress without private envelope
314314
CHECK_FALSE(contract::projectEvent(raw.dump(), "run", 8, prepared.context).valid);
315315
}
316316

317+
TEST_CASE("AutoConfig client omits probe credentials from public progress and results")
318+
{
319+
const auto prepared =
320+
prepare({{"streamSetup", "direct-single"}, {"outputs", json::array({standardOutput("horizontal", "horizontal", "twitch", twitchProbe())})}});
321+
const json rawEvent = {{"schemaVersion", 1},
322+
{"sessionId", "run"},
323+
{"sequence", 1},
324+
{"type", "progress"},
325+
{"phase", "bandwidth"},
326+
{"progress", 50},
327+
{"legId", "horizontal"},
328+
{"probeId", "twitch-probe"},
329+
{"provider", "twitch"},
330+
{"server", "rtmp://private.example/app"},
331+
{"streamKey", "event-secret"}};
332+
const auto event = contract::projectEvent(rawEvent.dump(), "run", -1, prepared.context);
333+
REQUIRE(event.valid);
334+
const json projectedEvent = json::parse(event.json);
335+
CHECK_FALSE(projectedEvent.contains("server"));
336+
CHECK_FALSE(projectedEvent.contains("streamKey"));
337+
CHECK(event.json.find("private.example") == std::string::npos);
338+
CHECK(event.json.find("event-secret") == std::string::npos);
339+
340+
json rawResult = {{"schemaVersion", 1},
341+
{"sessionId", "run"},
342+
{"status", "complete"},
343+
{"server", "rtmp://private.example/app"},
344+
{"streamKey", "result-secret"},
345+
{"legs", json::array({returnedStandardOutput("horizontal", "horizontal", "twitch", "twitch", "twitch-bandwidth-test", 5000, 4500)})}};
346+
rawResult["legs"][0]["measurement"]["probes"][0]["server"] = "rtmp://nested-private.example/app";
347+
rawResult["legs"][0]["measurement"]["probes"][0]["streamKey"] = "nested-result-secret";
348+
const auto result = contract::projectResult(rawResult.dump(), "run", prepared.context);
349+
REQUIRE(result.valid);
350+
const json projectedResult = json::parse(result.json);
351+
CHECK_FALSE(projectedResult.contains("server"));
352+
CHECK_FALSE(projectedResult.contains("streamKey"));
353+
CHECK_FALSE(projectedResult["outputs"][0]["measurement"]["evidence"][0].contains("server"));
354+
CHECK_FALSE(projectedResult["outputs"][0]["measurement"]["evidence"][0].contains("streamKey"));
355+
CHECK(result.json.find("private.example") == std::string::npos);
356+
CHECK(result.json.find("result-secret") == std::string::npos);
357+
}
358+
317359
TEST_CASE("AutoConfig client treats an empty poll as no event without consuming sequence")
318360
{
319361
const auto prepared =

obs-studio-client/tests/autoconfig-probe-policy-test.cpp

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,133 @@
44

55
namespace policy = autoConfig::probePolicy;
66

7+
TEST_CASE("Standard Twitch probes accept only official endpoints and bounded keys")
8+
{
9+
const std::string acceptedServers[] = {
10+
"auto",
11+
"AUTO",
12+
"rtmp://live.twitch.tv/app",
13+
"RTMPS://LIVE.TWITCH.TV/app",
14+
"rtmps://video-weaver.example.live-video.net:443/app",
15+
"rtmp://ingest.example.twitch.tv/app",
16+
};
17+
for (const std::string &server : acceptedServers) {
18+
CAPTURE(server);
19+
CHECK(policy::isOfficialTwitchServer(server));
20+
}
21+
22+
const std::string rejectedServers[] = {
23+
"",
24+
"live.twitch.tv",
25+
"http://live.twitch.tv/app",
26+
"rtmp:///app",
27+
"rtmp://user@live.twitch.tv/app",
28+
"rtmp://live.twitch.tv@evil.example/app",
29+
"rtmp://live.twitch.tv.evil.example/app",
30+
"rtmp://evil-live-video.net/app",
31+
"rtmp://twitch.tv.evil.example/app",
32+
};
33+
for (const std::string &server : rejectedServers) {
34+
CAPTURE(server);
35+
CHECK_FALSE(policy::isOfficialTwitchServer(server));
36+
}
37+
38+
CHECK(policy::isBoundedTwitchKey("live_123_example"));
39+
CHECK(policy::isBoundedTwitchKey(std::string(4096, 'k')));
40+
CHECK_FALSE(policy::isBoundedTwitchKey(""));
41+
CHECK_FALSE(policy::isBoundedTwitchKey(std::string(4097, 'k')));
42+
CHECK_FALSE(policy::isBoundedTwitchKey("live key"));
43+
CHECK_FALSE(policy::isBoundedTwitchKey("live\tkey"));
44+
CHECK_FALSE(policy::isBoundedTwitchKey(std::string("live\0key", 8)));
45+
}
46+
47+
TEST_CASE("Unbound YouTube probes accept only the official RTMPS endpoint and bounded keys")
48+
{
49+
const std::string acceptedServers[] = {
50+
"rtmps://a.rtmps.youtube.com/live2",
51+
"RTMPS://A.RTMPS.YOUTUBE.COM/live2",
52+
"rtmps://a.rtmps.youtube.com:443/live2",
53+
};
54+
for (const std::string &server : acceptedServers) {
55+
CAPTURE(server);
56+
CHECK(policy::isOfficialYoutubeRtmpsServer(server));
57+
}
58+
59+
const std::string rejectedServers[] = {
60+
"",
61+
"rtmp://a.rtmps.youtube.com/live2",
62+
"rtmps://a.rtmps.youtube.com",
63+
"rtmps://a.rtmps.youtube.com/",
64+
"rtmps://a.rtmps.youtube.com/live2/",
65+
"rtmps://b.rtmps.youtube.com/live2",
66+
"rtmps://sub.a.rtmps.youtube.com/live2",
67+
"rtmps://a.rtmps.youtube.com.evil.example/live2",
68+
"rtmps://user@a.rtmps.youtube.com/live2",
69+
"rtmps://a.rtmps.youtube.com:80/live2",
70+
"rtmps://a.rtmps.youtube.com:443:443/live2",
71+
"rtmps://a.rtmps.youtube.com/live2?query",
72+
"rtmps://a.rtmps.youtube.com/live2#fragment",
73+
"rtmps://a.rtmps.youtube.com/live 2",
74+
};
75+
for (const std::string &server : rejectedServers) {
76+
CAPTURE(server);
77+
CHECK_FALSE(policy::isOfficialYoutubeRtmpsServer(server));
78+
}
79+
80+
CHECK(policy::isBoundedYoutubeKey("abcd-1234_efgh.example"));
81+
CHECK(policy::isBoundedYoutubeKey(std::string(1024, 'k')));
82+
CHECK_FALSE(policy::isBoundedYoutubeKey(""));
83+
CHECK_FALSE(policy::isBoundedYoutubeKey(std::string(1025, 'k')));
84+
CHECK_FALSE(policy::isBoundedYoutubeKey("live key"));
85+
CHECK_FALSE(policy::isBoundedYoutubeKey("live\nkey"));
86+
CHECK_FALSE(policy::isBoundedYoutubeKey(std::string("live\0key", 8)));
87+
for (const char reserved : std::string("/\\?#@:")) {
88+
CAPTURE(reserved);
89+
CHECK_FALSE(policy::isBoundedYoutubeKey(std::string("live") + reserved + "key"));
90+
}
91+
}
92+
93+
TEST_CASE("Active probe eligibility preserves specific Dual Output denials")
94+
{
95+
const auto eligible = policy::decideActiveProbeEligibility(true, true, true, true, true, false, false);
96+
CHECK(eligible.eligible);
97+
CHECK(eligible.denialReason.empty());
98+
99+
const auto customProvider = policy::decideActiveProbeEligibility(false, true, true, true, true, false, false);
100+
CHECK_FALSE(customProvider.eligible);
101+
CHECK(customProvider.denialReason == "active_probe_not_eligible");
102+
103+
const auto nonOfficialEndpoint = policy::decideActiveProbeEligibility(true, true, true, false, true, false, false);
104+
CHECK_FALSE(nonOfficialEndpoint.eligible);
105+
CHECK(nonOfficialEndpoint.denialReason == "active_probe_not_eligible");
106+
107+
CHECK_FALSE(policy::decideActiveProbeEligibility(true, false, true, true, true, false, false).eligible);
108+
CHECK_FALSE(policy::decideActiveProbeEligibility(true, true, false, true, true, false, false).eligible);
109+
CHECK_FALSE(policy::decideActiveProbeEligibility(true, true, true, true, false, false, false).eligible);
110+
111+
const auto unsupportedJointPair = policy::decideActiveProbeEligibility(true, true, true, true, true, true, false);
112+
CHECK_FALSE(unsupportedJointPair.eligible);
113+
CHECK(unsupportedJointPair.denialReason == "dual_output_multiple_active_legs");
114+
115+
const auto supportedJointPair = policy::decideActiveProbeEligibility(true, true, true, true, true, true, true);
116+
CHECK(supportedJointPair.eligible);
117+
CHECK(supportedJointPair.denialReason.empty());
118+
119+
const auto duplicateJointProbe = policy::decideActiveProbeEligibility(true, true, true, true, false, true, true);
120+
CHECK_FALSE(duplicateJointProbe.eligible);
121+
CHECK(duplicateJointProbe.denialReason == "active_probe_not_eligible");
122+
}
123+
124+
TEST_CASE("Standard Dual Output probing requires distinct registered horizontal and vertical canvases")
125+
{
126+
CHECK(policy::standardDualOutputCanvasPairIsValid("horizontal", 1, true, "vertical", 2, true));
127+
CHECK(policy::standardDualOutputCanvasPairIsValid("vertical", 2, true, "horizontal", 1, true));
128+
CHECK_FALSE(policy::standardDualOutputCanvasPairIsValid("horizontal", 1, true, "vertical", 1, true));
129+
CHECK_FALSE(policy::standardDualOutputCanvasPairIsValid("horizontal", 1, false, "vertical", 2, true));
130+
CHECK_FALSE(policy::standardDualOutputCanvasPairIsValid("horizontal", 1, true, "vertical", 2, false));
131+
CHECK_FALSE(policy::standardDualOutputCanvasPairIsValid("horizontal", 1, true, "horizontal", 2, true));
132+
}
133+
7134
TEST_CASE("Provider probe coverage distinguishes absent, partial, and complete evidence")
8135
{
9136
CHECK(policy::classifyProviderProbeCoverage(2, 0) == policy::ProviderProbeCoverage::None);

obs-studio-client/tests/autoconfig-quality-policy-test.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,35 @@ TEST_CASE("Auto Config preserves probe headroom while capping the recommended bi
326326
CHECK(policy::select({1920, 1080, 60, 1}, 7900, "obs_nvenc_h264_tex").bitrateKbps == 7900);
327327
}
328328

329+
TEST_CASE("Auto Config composes estimated bitrate limits deterministically")
330+
{
331+
SECTION("the strictest destination limit wins")
332+
{
333+
CHECK(policy::composeEstimatedBitrateKbps(8000, 0, 6000) == 6000);
334+
}
335+
336+
SECTION("a lower current bitrate is preserved")
337+
{
338+
CHECK(policy::composeEstimatedBitrateKbps(2500, 0, 6000) == 2500);
339+
}
340+
341+
SECTION("the product limit applies without a destination limit")
342+
{
343+
CHECK(policy::composeEstimatedBitrateKbps(12000) == policy::kMaximumRecommendedBitrateKbps);
344+
}
345+
346+
SECTION("a request limit can be stricter than destination limits")
347+
{
348+
CHECK(policy::composeEstimatedBitrateKbps(8000, 1800, 6000) == 1800);
349+
}
350+
351+
SECTION("missing current bitrate uses the product default")
352+
{
353+
CHECK(policy::composeEstimatedBitrateKbps(0) == policy::kDefaultEstimatedBitrateKbps);
354+
CHECK(policy::composeEstimatedBitrateKbps(-1) == policy::kDefaultEstimatedBitrateKbps);
355+
}
356+
}
357+
329358
TEST_CASE("Auto Config benchmark ceiling explicitly permits isolated promotion above the current canvas")
330359
{
331360
const policy::VideoTuple current{1280, 720, 30, 1};

0 commit comments

Comments
 (0)