/*
* Copyright 2015-2016 Canonical Ltd.
* Copyright 2021-2023 Robert Tari
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Authors:
* Ted Gould
* Charles Kerr
* Robert Tari
*/
#include
#include
#include
#include
#include
#include
#include
#include "notifications-mock.h"
#include "gtest-gvariant.h"
extern "C" {
#include "indicator-sound-service.h"
#include "vala-mocks.h"
}
class NotificationsTest : public ::testing::Test
{
protected:
DbusTestService * service = NULL;
GDBusConnection * session = NULL;
std::shared_ptr notifications;
virtual void SetUp() {
g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE);
g_setenv("GSETTINGS_BACKEND", "memory", TRUE);
service = dbus_test_service_new(NULL);
dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_SESSION);
/* Useful for debugging test failures, not needed all the time (until it fails) */
#if 0
auto bustle = std::shared_ptr([]() {
DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new("notifications-test.bustle"));
dbus_test_task_set_name(bustle, "Bustle");
dbus_test_task_set_bus(bustle, DBUS_TEST_SERVICE_BUS_SESSION);
return bustle;
}(), [](DbusTestTask * bustle) {
g_clear_object(&bustle);
});
dbus_test_service_add_task(service, bustle.get());
#endif
notifications = std::make_shared();
dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
dbus_test_service_start_tasks(service);
session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
ASSERT_NE(nullptr, session);
g_dbus_connection_set_exit_on_close(session, FALSE);
g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
/* This is done in main.c */
notify_init("ayatana-indicator-sound");
}
virtual void TearDown() {
if (notify_is_initted())
notify_uninit();
notifications.reset();
g_clear_object(&service);
g_object_unref(session);
unsigned int cleartry = 0;
while (session != NULL && cleartry < 100) {
loop(100);
cleartry++;
}
ASSERT_EQ(nullptr, session);
}
static gboolean timeout_cb (gpointer user_data) {
GMainLoop * pLoop = static_cast(user_data);
g_main_loop_quit(pLoop);
return G_SOURCE_REMOVE;
}
void loop (unsigned int ms) {
GMainLoop * loop = g_main_loop_new(NULL, FALSE);
g_timeout_add(ms, timeout_cb, loop);
g_main_loop_run(loop);
g_main_loop_unref(loop);
}
void loop_until(const std::function& test, unsigned int max_ms=50, unsigned int test_interval_ms=10) {
// g_timeout's callback only allows a single pointer,
// so use a temporary stack struct to wedge everything into one pointer
struct CallbackData {
const std::function& test;
const gint64 deadline;
GMainLoop* loop = g_main_loop_new(nullptr, false);
CallbackData (const std::function& f, unsigned int max_ms):
test{f},
deadline{g_get_monotonic_time() + (max_ms*1000)} {}
~CallbackData() {g_main_loop_unref(loop);}
} data(test, max_ms);
// tell the timer to stop looping on success or deadline
auto timerfunc = [](gpointer gdata) -> gboolean {
auto& data = *static_cast(gdata);
if (!data.test() && (g_get_monotonic_time() < data.deadline))
return G_SOURCE_CONTINUE;
g_main_loop_quit(data.loop);
return G_SOURCE_REMOVE;
};
// start looping
g_timeout_add (std::min(max_ms, test_interval_ms), timerfunc, &data);
g_main_loop_run(data.loop);
}
void loop_until_notifications(unsigned int max_seconds=1) {
auto test = [this]{ return !notifications->getNotifications().empty(); };
loop_until(test, max_seconds);
}
static int unref_idle (gpointer user_data) {
g_variant_unref(static_cast(user_data));
return G_SOURCE_REMOVE;
}
std::shared_ptr playerListMock () {
auto playerList = std::shared_ptr(
MEDIA_PLAYER_LIST(media_player_list_mock_new()),
[](MediaPlayerList * list) {
g_clear_object(&list);
});
return playerList;
}
std::shared_ptr optionsMock () {
auto options = std::shared_ptr(
INDICATOR_SOUND_OPTIONS(options_mock_new()),
[](IndicatorSoundOptions * options){
g_clear_object(&options);
});
return options;
}
std::shared_ptr volumeControlMock (const std::shared_ptr& optionsMock) {
auto volumeControl = std::shared_ptr(
VOLUME_CONTROL(volume_control_mock_new(optionsMock.get())),
[](VolumeControl * control){
g_clear_object(&control);
});
return volumeControl;
}
std::shared_ptr volumeWarningMock (const std::shared_ptr& optionsMock) {
auto volumeWarning = std::shared_ptr(
VOLUME_WARNING(volume_warning_mock_new(optionsMock.get())),
[](VolumeWarning * warning){
g_clear_object(&warning);
});
return volumeWarning;
}
std::shared_ptr standardService (
const std::shared_ptr& volumeControl,
const std::shared_ptr& playerList,
const std::shared_ptr& options,
const std::shared_ptr& warning,
const std::shared_ptr& accounts_service_access) {
auto soundService = std::shared_ptr(
indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr, options.get(), warning.get(), accounts_service_access.get()),
[](IndicatorSoundService * service){
g_clear_object(&service);
});
return soundService;
}
void setMockVolume (std::shared_ptr volumeControl, double volume, VolumeControlVolumeReasons reason = VOLUME_CONTROL_VOLUME_REASONS_USER_KEYPRESS) {
VolumeControlVolume * vol = volume_control_volume_new();
vol->volume = volume;
vol->reason = reason;
volume_control_set_volume(volumeControl.get(), vol);
g_object_unref(vol);
}
void setIndicatorShown (bool shown) {
auto bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
g_dbus_connection_call(bus,
g_dbus_connection_get_unique_name(bus),
"/org/ayatana/indicator/sound",
"org.gtk.Actions",
"SetState",
g_variant_new("(sva{sv})", "indicator-shown", g_variant_new_boolean(shown), nullptr),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
nullptr,
nullptr);
g_clear_object(&bus);
}
};
TEST_F(NotificationsTest, BasicObject) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Give some time settle */
loop(50);
/* Auto free */
}
TEST_F(NotificationsTest, VolumeChanges) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.50);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_EQ("ayatana-indicator-sound", notev[0].app_name);
EXPECT_EQ("Volume", notev[0].summary);
EXPECT_EQ(0, notev[0].actions.size());
EXPECT_GVARIANT_EQ ("@s 'true'", notev[0].hints["x-lomiri-private-synchronous"]);
EXPECT_GVARIANT_EQ("@i 50", notev[0].hints["value"]);
/* Set a different volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.60);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ("@i 60", notev[0].hints["value"]);
/* Have pulse set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.70, VOLUME_CONTROL_VOLUME_REASONS_PULSE_CHANGE);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(0, notev.size());
/* Have AS set the volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.80, VOLUME_CONTROL_VOLUME_REASONS_ACCOUNTS_SERVICE_SET);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(0, notev.size());
}
TEST_F(NotificationsTest, DISABLED_StreamChanges) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.5);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
/* Change Streams, no volume change */
notifications->clearNotifications();
volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM);
setMockVolume(volumeControl, 0.5, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
loop(50);
notev = notifications->getNotifications();
EXPECT_EQ(0, notev.size());
/* Change Streams, volume change */
notifications->clearNotifications();
volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALERT);
setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
loop(50);
notev = notifications->getNotifications();
EXPECT_EQ(0, notev.size());
/* Change Streams, no volume change, volume up */
notifications->clearNotifications();
volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_MULTIMEDIA);
setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
loop(50);
setMockVolume(volumeControl, 0.65);
notev = notifications->getNotifications();
EXPECT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ("@i 65", notev[0].hints["value"]);
}
TEST_F(NotificationsTest, DISABLED_IconTesting) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set an initial volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.5);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
/* Generate a set of notifications */
notifications->clearNotifications();
for (float i = 0.0; i < 1.01; i += 0.1) {
setMockVolume(volumeControl, i);
}
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(11, notev.size());
EXPECT_EQ("audio-volume-muted", notev[0].app_icon);
EXPECT_EQ("audio-volume-low", notev[1].app_icon);
EXPECT_EQ("audio-volume-low", notev[2].app_icon);
EXPECT_EQ("audio-volume-medium", notev[3].app_icon);
EXPECT_EQ("audio-volume-medium", notev[4].app_icon);
EXPECT_EQ("audio-volume-medium", notev[5].app_icon);
EXPECT_EQ("audio-volume-medium", notev[6].app_icon);
EXPECT_EQ("audio-volume-high", notev[7].app_icon);
EXPECT_EQ("audio-volume-high", notev[8].app_icon);
EXPECT_EQ("audio-volume-high", notev[9].app_icon);
EXPECT_EQ("audio-volume-high", notev[10].app_icon);
}
TEST_F(NotificationsTest, DISABLED_ServerRestart) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.50);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
/* Restart server without sync notifications */
notifications->clearNotifications();
dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
notifications.reset();
loop(50);
notifications = std::make_shared(std::vector({"body", "body-markup", "icon-static"}));
dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
dbus_test_task_run((DbusTestTask*)*notifications);
/* Change the volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.60);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(0, notev.size());
/* Put a good server back */
dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
notifications.reset();
loop(50);
notifications = std::make_shared();
dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
dbus_test_task_run((DbusTestTask*)*notifications);
/* Change the volume again */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.70);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
}
TEST_F(NotificationsTest, DISABLED_HighVolume) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.50);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_EQ("Volume", notev[0].summary);
EXPECT_EQ("Speakers", notev[0].body);
EXPECT_GVARIANT_EQ ("@s 'false'", notev[0].hints["x-lomiri-value-bar-tint"]);
/* Set high volume with volume change */
notifications->clearNotifications();
volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true);
setMockVolume(volumeControl, 0.90);
loop(50);
notev = notifications->getNotifications();
ASSERT_LT(0, notev.size()); /* This passes with one or two since it would just be an update to the first if a second was sent */
EXPECT_EQ("Volume", notev[0].summary);
EXPECT_EQ("Speakers", notev[0].body);
EXPECT_GVARIANT_EQ ("@s 'true'", notev[0].hints["x-lomiri-value-bar-tint"]);
/* Move it back */
volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), false);
setMockVolume(volumeControl, 0.50);
loop(50);
/* Set high volume without level change */
/* NOTE: This can happen if headphones are plugged in */
notifications->clearNotifications();
volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_EQ("Volume", notev[0].summary);
EXPECT_EQ("Speakers", notev[0].body);
}
TEST_F(NotificationsTest, DISABLED_MenuHide) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.50);
loop(50);
auto notev = notifications->getNotifications();
EXPECT_EQ(1, notev.size());
/* Set the indicator to shown, and set a new volume */
notifications->clearNotifications();
setIndicatorShown(true);
loop(50);
setMockVolume(volumeControl, 0.60);
loop(50);
notev = notifications->getNotifications();
EXPECT_EQ(0, notev.size());
/* Set the indicator to hidden, and set a new volume */
notifications->clearNotifications();
setIndicatorShown(false);
loop(50);
setMockVolume(volumeControl, 0.70);
loop(50);
notev = notifications->getNotifications();
EXPECT_EQ(1, notev.size());
}
TEST_F(NotificationsTest, DISABLED_ExtendendVolumeNotification) {
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
// cppcheck-suppress unreadVariable
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
/* Set a volume */
notifications->clearNotifications();
setMockVolume(volumeControl, 0.50);
loop(50);
auto notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_EQ("ayatana-indicator-sound", notev[0].app_name);
EXPECT_EQ("Volume", notev[0].summary);
EXPECT_EQ(0, notev[0].actions.size());
EXPECT_GVARIANT_EQ("@i 50", notev[0].hints["value"]);
EXPECT_GVARIANT_EQ ("@s 'true'", notev[0].hints["x-lomiri-private-synchronous"]);
/* Allow an amplified volume */
notifications->clearNotifications();
volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM);
options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.5);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ("@i 33", notev[0].hints["value"]);
/* Set to 'over max' */
notifications->clearNotifications();
setMockVolume(volumeControl, 1.525);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ("@i 100", notev[0].hints["value"]);
/* Put back */
notifications->clearNotifications();
options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.0);
loop(50);
notev = notifications->getNotifications();
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ("@i 100", notev[0].hints["value"]);
}
TEST_F(NotificationsTest, DISABLED_TriggerWarning) {
// Tests all the conditions needed to trigger a volume warning.
// There are many possible combinations, so this test is slow. :P
const struct {
bool expected;
VolumeControlActiveOutput output;
} test_outputs[] = {
{ false, VOLUME_CONTROL_ACTIVE_OUTPUT_SPEAKERS },
{ true, VOLUME_CONTROL_ACTIVE_OUTPUT_HEADPHONES },
{ true, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_HEADPHONES },
{ false, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_SPEAKER },
{ false, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_SPEAKER },
{ true, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_HEADPHONES },
{ false, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_SPEAKER },
{ true, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_HEADPHONES },
{ false, VOLUME_CONTROL_ACTIVE_OUTPUT_CALL_MODE }
};
const struct {
bool expected;
pa_volume_t volume;
pa_volume_t loud_volume;
} test_volumes[] = {
{ false, 50, 100 },
{ false, 99, 100 },
{ false, 100, 100 }, // Whenever you increase volume... such that acoustic output would be *MORE* than 85 dBA
{ true, 101, 100 }
};
const struct {
bool expected;
bool approved;
} test_approved[] = {
{ true, false },
{ false, true }
};
const struct {
bool expected;
bool warnings_enabled;
} test_warnings_enabled[] = {
{ true, true },
{ false, false }
};
const struct {
bool expected;
bool multimedia_active;
} test_multimedia_active[] = {
{ true, true },
{ false, false }
};
for (const auto& outputs : test_outputs) {
for (const auto& volumes : test_volumes) {
for (const auto& approved : test_approved) {
for (const auto& warnings_enabled : test_warnings_enabled) {
for (const auto& multimedia_active : test_multimedia_active) {
notifications->clearNotifications();
// instantiate the test subjects
auto options = optionsMock();
auto volumeControl = volumeControlMock(options);
auto volumeWarning = volumeWarningMock(options);
auto accountsService = std::make_shared();
auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning, accountsService);
// run the test
options_mock_mock_set_loud_volume(OPTIONS_MOCK(options.get()), volumes.loud_volume);
options_mock_mock_set_loud_warning_enabled(OPTIONS_MOCK(options.get()), warnings_enabled.warnings_enabled);
volume_warning_mock_set_approved(VOLUME_WARNING_MOCK(volumeWarning.get()), approved.approved);
volume_warning_mock_set_multimedia_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), volumes.volume);
volume_warning_mock_set_multimedia_active(VOLUME_WARNING_MOCK(volumeWarning.get()), multimedia_active.multimedia_active);
volume_control_mock_mock_set_active_output(VOLUME_CONTROL_MOCK(volumeControl.get()), outputs.output);
loop_until_notifications();
// check the result
auto notev = notifications->getNotifications();
const bool warning_expected = outputs.expected && volumes.expected && approved.expected && warnings_enabled.expected && multimedia_active.expected;
if (warning_expected) {
EXPECT_TRUE(volume_warning_get_active(volumeWarning.get()));
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ ("@s 'true'", notev[0].hints["x-lomiri-snap-decisions"]);
EXPECT_GVARIANT_EQ (nullptr, notev[0].hints["x-lomiri-private-synchronous"]);
}
else {
EXPECT_FALSE(volume_warning_get_active(volumeWarning.get()));
ASSERT_EQ(1, notev.size());
EXPECT_GVARIANT_EQ (nullptr, notev[0].hints["x-lomiri-snap-decisions"]);
EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-lomiri-private-synchronous"]);
}
} // multimedia_active
} // warnings_enabled
} // approved
} // volumes
} // outputs
}