Skip to content

Commit e2dbaec

Browse files
test(tray): add system tray tests
1 parent 73f84fb commit e2dbaec

File tree

4 files changed

+329
-1
lines changed

4 files changed

+329
-1
lines changed

.github/workflows/ci-linux.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ jobs:
160160
run: |
161161
sudo apt-get update -y
162162
sudo apt-get install -y \
163+
at-spi2-core \
164+
dbus-x11 \
163165
x11-xserver-utils \
164166
xvfb
165167
@@ -171,11 +173,30 @@ jobs:
171173
id: test
172174
working-directory: build/tests
173175
run: |
176+
# Start D-Bus session bus
177+
export $(dbus-launch --sh-syntax)
178+
echo "D-Bus session started with SESSION_MANAGER=$DBUS_SESSION_BUS_ADDRESS"
179+
180+
# Start accessibility services
181+
/usr/libexec/at-spi-bus-launcher &
182+
AT_SPI_PID=$!
183+
184+
# Start X11 display
174185
export DISPLAY=:1
175186
Xvfb ${DISPLAY} -screen 0 1024x768x24 &
187+
XVFB_PID=$!
176188
sleep 5 # give Xvfb time to start
177189
190+
# Run the tests with proper cleanup
178191
./test_sunshine --gtest_color=yes --gtest_output=xml:test_results.xml
192+
TEST_EXIT_CODE=$?
193+
194+
# Clean up background processes
195+
kill $XVFB_PID 2>/dev/null || true
196+
kill $AT_SPI_PID 2>/dev/null || true
197+
kill $DBUS_SESSION_BUS_PID 2>/dev/null || true
198+
199+
exit $TEST_EXIT_CODE
179200
180201
- name: Generate gcov report
181202
id: test_report

scripts/linux_build.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ function add_arch_deps() {
128128
'opus'
129129
'udev'
130130
'wayland'
131+
'dbus' # D-Bus for system tray support
132+
'at-spi2-core' # AT-SPI accessibility services
131133
)
132134

133135
if [ "$skip_libva" == 0 ]; then
@@ -181,6 +183,8 @@ function add_debian_based_deps() {
181183
"udev"
182184
"wget" # necessary for cuda install with `run` file
183185
"xvfb" # necessary for headless unit testing
186+
"dbus-x11" # D-Bus session bus for system tray tests
187+
"at-spi2-core" # AT-SPI accessibility services for system tray
184188
)
185189

186190
if [ "$skip_libva" == 0 ]; then
@@ -251,6 +255,8 @@ function add_fedora_deps() {
251255
"wget" # necessary for cuda install with `run` file
252256
"which" # necessary for cuda install with `run` file
253257
"xorg-x11-server-Xvfb" # necessary for headless unit testing
258+
"dbus-x11" # D-Bus session bus for system tray tests
259+
"at-spi2-core" # AT-SPI accessibility services for system tray
254260
)
255261

256262
if [ "$skip_libva" == 0 ]; then

src/system_tray.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ namespace system_tray {
191191
// Wait for the shell to be initialized before registering the tray icon.
192192
// This ensures the tray icon works reliably after a logoff/logon cycle.
193193
while (GetShellWindow() == nullptr) {
194-
Sleep(1000);
194+
Sleep(1000); // TODO: this delay causes unit tests to take ~10s for each tray initialization
195195
}
196196
#endif
197197

tests/unit/test_system_tray.cpp

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/**
2+
* @file tests/unit/test_system_tray.cpp
3+
* @brief Test src/system_tray.*.
4+
*/
5+
#include "../tests_common.h"
6+
#include "../tests_log_checker.h"
7+
8+
#include <chrono>
9+
#include <thread>
10+
11+
// Only test the system tray if it's enabled
12+
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
13+
14+
#include <src/system_tray.h>
15+
16+
namespace {
17+
constexpr auto log_file = "test_sunshine.log";
18+
19+
// Helper class to manage tray lifecycle in tests
20+
class TrayTestHelper {
21+
public:
22+
static void cleanup_any_existing_tray() {
23+
// Ensure no tray is running before starting tests
24+
system_tray::end_tray();
25+
system_tray::end_tray_threaded();
26+
}
27+
};
28+
} // namespace
29+
30+
class SystemTrayTest: public testing::Test {
31+
protected:
32+
void SetUp() override {
33+
TrayTestHelper::cleanup_any_existing_tray();
34+
}
35+
36+
void TearDown() override {
37+
TrayTestHelper::cleanup_any_existing_tray();
38+
}
39+
};
40+
41+
class SystemTrayThreadedTest: public testing::Test {
42+
protected:
43+
void SetUp() override {
44+
TrayTestHelper::cleanup_any_existing_tray();
45+
}
46+
47+
void TearDown() override {
48+
TrayTestHelper::cleanup_any_existing_tray();
49+
}
50+
};
51+
52+
// Test basic tray initialization
53+
TEST_F(SystemTrayTest, InitTray) {
54+
// Note: This test may fail in CI environments without a display
55+
// The test verifies the function doesn't crash and returns a status
56+
const int result = system_tray::init_tray();
57+
58+
// The result should be either 0 (success) or 1 (failure, e.g., no display)
59+
EXPECT_TRUE(result == 0 || result == 1);
60+
61+
if (result == 0) {
62+
// If initialization succeeded, we should be able to clean up
63+
EXPECT_EQ(0, system_tray::end_tray());
64+
}
65+
}
66+
67+
// Test tray event processing
68+
TEST_F(SystemTrayTest, ProcessTrayEvents) {
69+
if (const int init_result = system_tray::init_tray(); init_result == 0) {
70+
// If the tray was initialized successfully, test event processing
71+
const int process_result = system_tray::process_tray_events();
72+
EXPECT_EQ(0, process_result);
73+
74+
// Clean up
75+
EXPECT_EQ(0, system_tray::end_tray());
76+
} else {
77+
// If no tray available, processing should fail gracefully
78+
int process_result = system_tray::process_tray_events();
79+
EXPECT_NE(0, process_result);
80+
}
81+
}
82+
83+
// Test tray update functions don't crash
84+
TEST_F(SystemTrayTest, UpdateTrayFunctions) {
85+
const std::string test_app = "TestApp";
86+
87+
// These functions should not crash even if the tray is not initialized
88+
EXPECT_NO_THROW(system_tray::update_tray_playing(test_app));
89+
EXPECT_NO_THROW(system_tray::update_tray_pausing(test_app));
90+
EXPECT_NO_THROW(system_tray::update_tray_stopped(test_app));
91+
EXPECT_NO_THROW(system_tray::update_tray_require_pin());
92+
}
93+
94+
// Test tray update functions with an initialized tray
95+
TEST_F(SystemTrayTest, UpdateTrayWithInitializedTray) {
96+
if (int init_result = system_tray::init_tray(); init_result == 0) {
97+
const std::string test_app = "TestApp";
98+
99+
// These should work without crashing when tray is initialized
100+
EXPECT_NO_THROW(system_tray::update_tray_playing(test_app));
101+
EXPECT_NO_THROW(system_tray::update_tray_pausing(test_app));
102+
EXPECT_NO_THROW(system_tray::update_tray_stopped(test_app));
103+
EXPECT_NO_THROW(system_tray::update_tray_require_pin());
104+
105+
// Clean up
106+
EXPECT_EQ(0, system_tray::end_tray());
107+
}
108+
}
109+
110+
// Test ending tray without initialization
111+
TEST_F(SystemTrayTest, EndTrayWithoutInit) {
112+
// Should be safe to call end_tray even if not initialized
113+
EXPECT_EQ(0, system_tray::end_tray());
114+
}
115+
116+
// Test threaded tray initialization
117+
TEST_F(SystemTrayThreadedTest, InitTrayThreaded) {
118+
const int result = system_tray::init_tray_threaded();
119+
120+
// The result should be either 0 (success) or 1 (failure, e.g., no display)
121+
EXPECT_TRUE(result == 0 || result == 1);
122+
123+
if (result == 0) {
124+
// Give the thread a moment to start
125+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
126+
127+
// Verify we can stop the threaded tray
128+
EXPECT_EQ(0, system_tray::end_tray_threaded());
129+
}
130+
}
131+
132+
// Test double initialization of a threaded tray
133+
TEST_F(SystemTrayThreadedTest, DoubleInitTrayThreaded) {
134+
if (const int first_result = system_tray::init_tray_threaded(); first_result == 0) {
135+
// Give the thread a moment to start
136+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
137+
138+
// Second initialization should fail
139+
const int second_result = system_tray::init_tray_threaded();
140+
EXPECT_EQ(1, second_result);
141+
142+
// Check that a warning message was logged
143+
EXPECT_TRUE(log_checker::line_contains(log_file, "Tray thread is already running"));
144+
145+
// Clean up
146+
EXPECT_EQ(0, system_tray::end_tray_threaded());
147+
}
148+
}
149+
150+
// Test ending threaded tray without initialization
151+
TEST_F(SystemTrayThreadedTest, EndThreadedTrayWithoutInit) {
152+
// Should be safe to call end_tray_threaded even if not initialized
153+
EXPECT_EQ(0, system_tray::end_tray_threaded());
154+
}
155+
156+
// Test threaded tray lifecycle
157+
TEST_F(SystemTrayThreadedTest, ThreadedTrayLifecycle) {
158+
if (int init_result = system_tray::init_tray_threaded(); init_result == 0) {
159+
// Give the thread time to start and initialize
160+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
161+
162+
// Check that an initialization message was logged
163+
EXPECT_TRUE(log_checker::line_contains(log_file, "System tray thread initialized successfully"));
164+
165+
// Test tray updates work with a threaded tray
166+
const std::string test_app = "ThreadedTestApp";
167+
EXPECT_NO_THROW(system_tray::update_tray_playing(test_app));
168+
EXPECT_NO_THROW(system_tray::update_tray_pausing(test_app));
169+
EXPECT_NO_THROW(system_tray::update_tray_stopped(test_app));
170+
EXPECT_NO_THROW(system_tray::update_tray_require_pin());
171+
172+
// Stop the threaded tray
173+
EXPECT_EQ(0, system_tray::end_tray_threaded());
174+
175+
// Give the thread time to stop
176+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
177+
178+
// Check that a stop message was logged
179+
EXPECT_TRUE(log_checker::line_contains(log_file, "System tray thread stopped"));
180+
}
181+
}
182+
183+
// Test that main-thread and threaded tray don't interfere
184+
TEST_F(SystemTrayTest, MainThreadAndThreadedTrayIsolation) {
185+
// Initialize a main thread tray first
186+
187+
if (const int main_result = system_tray::init_tray(); main_result == 0) {
188+
// Try to initialize threaded tray - should work independently
189+
190+
if (const int threaded_result = system_tray::init_tray_threaded(); threaded_result == 0) {
191+
// Give a threaded tray time to start
192+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
193+
194+
// Both should be able to clean up independently
195+
EXPECT_EQ(0, system_tray::end_tray());
196+
EXPECT_EQ(0, system_tray::end_tray_threaded());
197+
} else {
198+
// Clean up the main thread tray
199+
EXPECT_EQ(0, system_tray::end_tray());
200+
}
201+
}
202+
}
203+
204+
// Test rapid start/stop cycles
205+
TEST_F(SystemTrayThreadedTest, RapidStartStopCycles) {
206+
// First, check if tray initialization is possible in this environment
207+
BOOST_LOG(info) << "Testing tray initialization capability...";
208+
209+
if (const int test_init_result = system_tray::init_tray_threaded(); test_init_result != 0) {
210+
// Try a regular tray initialization to see if it's a threading issue
211+
if (const int regular_init_result = system_tray::init_tray(); regular_init_result == 0) {
212+
BOOST_LOG(info) << "Regular tray initialization succeeded, but threaded failed";
213+
system_tray::end_tray();
214+
GTEST_SKIP() << "Threaded tray initialization failed (code: " << test_init_result
215+
<< "), but regular tray works. May be a threading/timing issue in test environment.";
216+
} else {
217+
BOOST_LOG(info) << "Both regular and threaded tray initialization failed - no display available";
218+
// Instead of skipping, let's test the threading logic without actual tray
219+
BOOST_LOG(info) << "Testing threading functionality without display...";
220+
221+
// Test that the threading functions don't crash and return appropriate error codes
222+
EXPECT_EQ(1, test_init_result); // Should fail with code 1 (no display)
223+
EXPECT_EQ(1, regular_init_result); // Should fail with code 1 (no display)
224+
225+
// Test multiple calls to init_tray_threaded when no display is available
226+
const int second_init_result = system_tray::init_tray_threaded();
227+
EXPECT_EQ(1, second_init_result); // Should consistently fail
228+
229+
// Test that end_tray_threaded is safe to call even when init failed
230+
EXPECT_EQ(0, system_tray::end_tray_threaded()); // Should always return 0
231+
232+
// Test that update functions don't crash when no tray is available
233+
const std::string test_app = "NoDisplayTestApp";
234+
EXPECT_NO_THROW(system_tray::update_tray_playing(test_app));
235+
EXPECT_NO_THROW(system_tray::update_tray_pausing(test_app));
236+
EXPECT_NO_THROW(system_tray::update_tray_stopped(test_app));
237+
EXPECT_NO_THROW(system_tray::update_tray_require_pin());
238+
239+
BOOST_LOG(info) << "Threading functionality tested successfully (no display mode)";
240+
return; // Test passed - we validated the threading logic works correctly
241+
}
242+
}
243+
244+
BOOST_LOG(info) << "Tray initialization succeeded, proceeding with controlled cycles test";
245+
246+
// Clean up the test initialization
247+
EXPECT_EQ(0, system_tray::end_tray_threaded());
248+
249+
// Note: The Windows system tray has limitations on rapid reinitialization
250+
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Longer wait for full cleanup
251+
BOOST_LOG(info) << "Starting controlled start/stop cycle";
252+
253+
if (const int init_result = system_tray::init_tray_threaded(); init_result == 0) {
254+
BOOST_LOG(info) << "Cycle completed successfully - threaded tray can be reinitialized";
255+
256+
// Give the thread time to start properly
257+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
258+
259+
// Test some tray operations while it's running
260+
const std::string test_app = "CycleTestApp";
261+
EXPECT_NO_THROW(system_tray::update_tray_playing(test_app));
262+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
263+
264+
EXPECT_NO_THROW(system_tray::update_tray_stopped(test_app));
265+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
266+
267+
// Stop the tray
268+
const int stop_result = system_tray::end_tray_threaded();
269+
EXPECT_EQ(0, stop_result);
270+
271+
BOOST_LOG(info) << "Controlled cycle test completed successfully";
272+
} else {
273+
FAIL() << "Tray reinitialization not supported in this environment. "
274+
<< "Initial test passed but subsequent initialization failed with code: " << init_result;
275+
}
276+
}
277+
278+
// Performance test - verify thread startup time is reasonable
279+
TEST_F(SystemTrayThreadedTest, ThreadStartupPerformance) {
280+
const auto start_time = std::chrono::steady_clock::now();
281+
282+
const int result = system_tray::init_tray_threaded();
283+
284+
const auto end_time = std::chrono::steady_clock::now();
285+
const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
286+
287+
if (result == 0) {
288+
// Startup should complete within 5 seconds (much less in practice)
289+
EXPECT_LT(duration.count(), 5000);
290+
291+
// Clean up
292+
EXPECT_EQ(0, system_tray::end_tray_threaded());
293+
}
294+
}
295+
296+
#else
297+
// If the tray is not enabled, provide a simple test that passes
298+
TEST(SystemTrayDisabled, TrayNotEnabled) {
299+
GTEST_SKIP() << "System tray is not enabled in this build";
300+
}
301+
#endif

0 commit comments

Comments
 (0)