aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt26
-rw-r--r--tests/manual99
-rw-r--r--tests/pa-mock.cpp569
-rw-r--r--tests/volume-control-test.cc83
4 files changed, 777 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f588d12..c1b4afc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -83,6 +83,16 @@ target_link_libraries(
include_directories(${CMAKE_CURRENT_BINARY_DIR})
###########################
+# Pulse Mock
+###########################
+
+add_library(
+ pulse-mock
+ SHARED
+ pa-mock.cpp
+)
+
+###########################
# Name Watch Test
###########################
@@ -116,6 +126,22 @@ add_test(accounts-service-user-test-player
)
###########################
+# Volume Control
+###########################
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+add_executable (volume-control-test volume-control-test.cc)
+target_link_libraries (
+ volume-control-test
+ indicator-sound-service-lib
+ pulse-mock
+ gtest
+ ${TEST_LIBRARIES}
+)
+
+add_test(volume-control-test volume-control-test)
+
+###########################
# Sound Menu
###########################
diff --git a/tests/manual b/tests/manual
index 201465c..c1cc214 100644
--- a/tests/manual
+++ b/tests/manual
@@ -22,3 +22,102 @@ Test-case indicator-sound/unity8-items-check
<dd>The menu is populated with items</dd>
</dl>
+Test-case indicator-sound/unity8-sound-notifications
+<dl>
+ <dt>Adjust volume using HW keys if available</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>An audibule sound should play at the level of the audio</dd>
+ <dt>Adjust volume with slider in sound indicator</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>An audibule sound should play at the level of the audio</dd>
+ <dt>Open a video with sound and play in media player</dt>
+ <dd>The video should play and the sound should be audible</dd>
+ <dt>Adjust volume using HW keys if available</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>No notification sound should be heard</dd>
+ <dt>Adjust volume with slider in sound indicator</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>No notification sound should be heard</dd>
+</dl>
+
+Test-case indicator-sound/unity8-high-volume
+<dl>
+ <dt>Plug headphones into the headphone jack</dt>
+ <dt>Adjust volume so that it is at the midpoint of volume range</dt>
+ <dd>The slider should be in the middle of the scale</dd>
+ <dt>Increase the volume once using HW keys if available</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>There should be no text on the notification</dd>
+ <dt>Increase the volume using HW keys until it is roughly 90% of the range</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>The text on the notification should read "High volume"</dd>
+ <dd>The range on the notification bubble should have a different color signifying the higher volume</dd>
+ <dt>Decrease the volume using HW keys until it is roughly 50% of the range</dt>
+ <dd>A notification bubble should appear with the sound volume</dd>
+ <dd>There should be no text on the notification</dd>
+ <dd>The range on the notification bubble should have a standard color</dd>
+</dl>
+
+Test-case indicator-sound/unity8-silent-mode
+<dl>
+ <dt>NOTE: This test currently doesn't work because of a bug: http://pad.lv/1336715</dt>
+ <dt>Open the Sound menu</dt>
+ <dd>The sound menu includes an item "Silent Mode" which is a check box</dd>
+ <dd>The checkbox is not checked</dd>
+ <dt>Enable silent mode</dt>
+ <dd>Selecting the "Silent Mode" item should cause the box to be checked</dd>
+ <dt>Open the sound panel in system settings</dt>
+ <dd>The sound panel includes an item "Silent Mode" which is a check box</dd>
+ <dd>The checkbox is checked</dd>
+ <dt>Disable silent mode in system settings</dt>
+ <dd>The checkbox is not checked</dd>
+ <dt>Open the Sound menu</dt>
+ <dd>The sound menu includes an item "Silent Mode" which is a check box</dd>
+ <dd>The checkbox is not checked</dd>
+</dl>
+
+Test-case indicator-sound/unity8-audio-roles
+<dl>
+ <dt>Without playing anything (no active audio stream), change the volume on the indicator or with the volume buttons and then try playing one of the following audio streams: camera shutter, ringtone, message notification, dtmf</dt>
+ <dd>The audio stream should reflect the volume set on the indicator</dd>
+ <dt>Without playing anything (no active audio stream), change the volume on the indicator or with volume buttons and then try playing one of the following audio streams: music-app, webrowser (youtube)</dt>
+ <dd>The audio stream should not be affected by the volume set on the indicator when there was no other active stream</dt>
+ <dt>Play a multimedia stream (music-app, webrowser) and change the volume on the indicator when the stream is active</dt>
+ <dd>The multimedia audio stream should reflect the volume set on the indicator</dd>
+ <dd>When stopping/closing the multimedia stream, it should automatically show up the volume for the alert role (ringtone, notification, etc)</dd>
+ <dd>No other role should be affected by the volume level used by the multimedia role</dd>
+ <dt>Play a alarm stream (clock-app) and change the volume on the indicator when the stream is active</dt>
+ <dd>The alarm audio stream should reflect the volume set on the indicator</dd>
+ <dd>When stopping/closing the alarm stream, it should automatically show up the volume for the alert role (ringtone, notification, etc)</dd>
+ <dd>No other role should be affected by the volume level used by the alarm role</dd>
+ <dt>Start a voice call using the dialer-app and change the volume on the indicator when the call is active</dt>
+ <dd>The phone audio stream should reflect the volume set on the indicator</dd>
+ <dd>When hanging up the voice call it should automatically show up the volume for the alert role (ringtone, notification, etc)</dd>
+ <dd>No other role should be affected by the volume level used by the phone role</dd>
+</dl>
+
+Test-case indicator-sound/unity8-embedded-greeter
+<dl>
+ <dt>NOTE: Only works with embedded greeter, split greeter will require modifications to this test</dt>
+ <dt>Ensure System Settings is set to "Show Messages on Greeter"</dt>
+ <dt>Play a song in the media player</dt>
+ <dd>The song should be heard</dd>
+ <dd>There should be an entry in the sound menu with the meta data for the song being played</dd>
+ <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt>
+ <dt>Ensure the sound menu has song meta data</dt>
+ <dd>There should be an entry in the sound menu with the meta data for the song being played</dd>
+ <dt>Pause the song in the greeter</dt>
+ <dd>The song should stop playing</dd>
+ <dt>Resume the song in the greeter</dt>
+ <dd>The song should continue to play</dd>
+ <dt>Disable System Settings value "Show Messages on Greeter"</dt>
+ <dt>Ensure the sound menu has song meta data</dt>
+ <dd>There should be an entry in the sound menu with the meta data for the song being played</dd>
+ <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt>
+ <dt>Ensure the sound menu does not have song meta data</dt>
+ <dd>There should be an entry for the player but it should have no information on the song being played</dd>
+ <dt>Pause the song in the greeter</dt>
+ <dd>The song should stop playing</dd>
+ <dt>Resume the song in the greeter</dt>
+ <dd>The song should continue to play</dd>
+</dl>
diff --git a/tests/pa-mock.cpp b/tests/pa-mock.cpp
new file mode 100644
index 0000000..8ca2374
--- /dev/null
+++ b/tests/pa-mock.cpp
@@ -0,0 +1,569 @@
+/*
+ * Copyright © 2015 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <atomic>
+#include <functional>
+#include <vector>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/glib-mainloop.h>
+#include <gio/gio.h>
+#include <math.h>
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "PA-Mock"
+
+/* Core class of the PA Mock state */
+class PAMockContext {
+public:
+ /* Accounting */
+ std::atomic<unsigned int> refcnt;
+
+ /* State stuff */
+ std::vector<std::function<void(void)>> stateCallbacks;
+ pa_context_state_t currentState;
+ pa_context_state_t futureState;
+
+ /* Event stuff */
+ std::vector<std::function<void(pa_subscription_event_type_t, uint32_t)>> eventCallbacks;
+ pa_subscription_mask_t eventMask;
+
+ PAMockContext ()
+ : refcnt(1)
+ , currentState(PA_CONTEXT_UNCONNECTED)
+ , futureState(PA_CONTEXT_UNCONNECTED)
+ , eventMask(PA_SUBSCRIPTION_MASK_NULL)
+ {
+ g_debug("Creating Context: %p", this);
+ }
+
+ ~PAMockContext () {
+ g_debug("Destroying Context: %p", this);
+ }
+
+ /* Ref counting */
+ void ref () {
+ refcnt++;
+ }
+
+ void unref () {
+ refcnt--;
+ if (refcnt == 0)
+ delete this;
+ }
+
+ /* C/C++ boundry */
+ operator pa_context *() {
+ return reinterpret_cast<pa_context *>(this);
+ }
+
+ /* State Stuff */
+ void setState (pa_context_state_t state)
+ {
+ if (state == currentState)
+ return;
+
+ currentState = state;
+ for (auto callback : stateCallbacks) {
+ callback();
+ }
+ }
+
+ void idleOnce (std::function<void(void)> idleFunc) {
+ auto allocated = new std::function<void(void)>(idleFunc);
+
+ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
+ [](gpointer data) -> gboolean {
+ std::function<void(void)> * pidleFunc = reinterpret_cast<std::function<void(void)> *>(data);
+ (*pidleFunc)();
+ return G_SOURCE_REMOVE;
+ },
+ allocated,
+ [](gpointer data) -> void {
+ std::function<void(void)> * pidleFunc = reinterpret_cast<std::function<void(void)> *>(data);
+ delete pidleFunc;
+ });
+ }
+
+ void queueState (pa_context_state_t state)
+ {
+ idleOnce([this, state](){
+ setState(state);
+ });
+ }
+
+ pa_context_state_t getState (void)
+ {
+ return currentState;
+ }
+
+ void addStateCallback (std::function<void(void)> &callback)
+ {
+ stateCallbacks.push_back(callback);
+ }
+
+ /* Event Stuff */
+ void setMask (pa_subscription_mask_t mask)
+ {
+ eventMask = mask;
+ }
+
+ void addEventCallback (std::function<void(pa_subscription_event_type_t, uint32_t)> &callback)
+ {
+ eventCallbacks.push_back(callback);
+ }
+};
+
+/* *******************************
+ * context.h
+ * *******************************/
+
+pa_context *
+pa_context_new_with_proplist (pa_mainloop_api *mainloop, const char *name, pa_proplist *proplist)
+{
+ return *(new PAMockContext());
+}
+
+void
+pa_context_unref (pa_context *c) {
+ reinterpret_cast<PAMockContext*>(c)->unref();
+}
+
+pa_context *
+pa_context_ref (pa_context *c) {
+ reinterpret_cast<PAMockContext*>(c)->ref();
+ return c;
+}
+
+int
+pa_context_connect (pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api)
+{
+ reinterpret_cast<PAMockContext*>(c)->queueState(PA_CONTEXT_READY);
+ return 0;
+}
+
+void
+pa_context_disconnect (pa_context *c)
+{
+ reinterpret_cast<PAMockContext*>(c)->queueState(PA_CONTEXT_UNCONNECTED);
+}
+
+int
+pa_context_errno (pa_context *c)
+{
+ return 0;
+}
+
+void
+pa_context_set_state_callback (pa_context *c, pa_context_notify_cb_t cb, void *userdata)
+{
+ std::function<void(void)> cppcb([c, cb, userdata]() {
+ cb(c, userdata);
+ });
+ reinterpret_cast<PAMockContext*>(c)->addStateCallback(cppcb);
+}
+
+pa_context_state_t
+pa_context_get_state (pa_context *c)
+{
+ return reinterpret_cast<PAMockContext*>(c)->getState();
+}
+
+/* *******************************
+ * introspect.h
+ * *******************************/
+
+static pa_operation *
+dummy_operation (void)
+{
+ GObject * goper = (GObject *)g_object_new(G_TYPE_OBJECT, nullptr);
+ pa_operation * oper = (pa_operation *)goper;
+ return oper;
+}
+
+pa_operation*
+pa_context_get_server_info (pa_context *c, pa_server_info_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, cb, userdata]() {
+ if (cb == nullptr)
+ return;
+
+ pa_server_info server{
+ .user_name = "user",
+ .host_name = "host",
+ .server_version = "1.2.3",
+ .server_name = "server",
+ .sample_spec = {
+ .format = PA_SAMPLE_U8,
+ .rate = 44100,
+ .channels = 1
+ },
+ .default_sink_name = "default-sink",
+ .default_source_name = "default-source",
+ .cookie = 1234,
+ .channel_map = {
+ .channels = 0
+ }
+ };
+
+ cb(c, &server, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_get_sink_info_by_name (pa_context *c, const gchar * name, pa_sink_info_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, name, cb, userdata]() {
+ if (cb == nullptr)
+ return;
+
+ pa_sink_port_info active_port = {0};
+ active_port.name = "speaker";
+
+ pa_sink_info sink = {0};
+ sink.name = "default-sink";
+ sink.index = 0;
+ sink.description = "Default Sink";
+ sink.channel_map.channels = 0;
+ sink.active_port = &active_port;
+
+ cb(c, &sink, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_get_sink_info_list (pa_context *c, pa_sink_info_cb_t cb, void *userdata)
+{
+ /* Only have one today, so this is the same */
+ return pa_context_get_sink_info_by_name(c, "default-sink", cb, userdata);
+}
+
+pa_operation *
+pa_context_get_sink_input_info (pa_context *c, uint32_t idx, pa_sink_input_info_cb_t cb, void * userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, idx, cb, userdata]() {
+ if (cb == nullptr)
+ return;
+
+ pa_sink_input_info sink = { 0 };
+
+ cb(c, &sink, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_get_source_info_by_name (pa_context *c, const char * name, pa_source_info_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, name, cb, userdata]() {
+ if (cb == nullptr)
+ return;
+
+ pa_source_info source = {
+ .name = "default-source"
+ };
+
+ cb(c, &source, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_get_source_output_info (pa_context *c, uint32_t idx, pa_source_output_info_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, idx, cb, userdata]() {
+ if (cb == nullptr)
+ return;
+
+ pa_source_output_info source = {0};
+ source.name = "default source";
+
+ cb(c, &source, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_set_sink_mute_by_index (pa_context *c, uint32_t idx, int mute, pa_context_success_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, idx, mute, cb, userdata]() {
+ if (cb != nullptr)
+ cb(c, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_set_sink_volume_by_index (pa_context *c, uint32_t idx, const pa_cvolume * cvol, pa_context_success_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, idx, cvol, cb, userdata]() {
+ if (cb != nullptr)
+ cb(c, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+pa_operation*
+pa_context_set_source_volume_by_name (pa_context *c, const char * name, const pa_cvolume * cvol, pa_context_success_cb_t cb, void *userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, name, cvol, cb, userdata]() {
+ if (cb != nullptr)
+ cb(c, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+/* *******************************
+ * subscribe.h
+ * *******************************/
+
+pa_operation *
+pa_context_subscribe (pa_context * c, pa_subscription_mask_t mask, pa_context_success_cb_t callback, void * userdata)
+{
+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
+ [c, mask, callback, userdata]() {
+ reinterpret_cast<PAMockContext*>(c)->setMask(mask);
+ if (callback != nullptr)
+ callback(c, 1, userdata);
+ });
+
+ return dummy_operation();
+}
+
+void
+pa_context_set_subscribe_callback (pa_context * c, pa_context_subscribe_cb_t callback, void * userdata)
+{
+ std::function<void(pa_subscription_event_type_t, uint32_t)> cppcb([c, callback, userdata](pa_subscription_event_type_t event, uint32_t index) {
+ if (callback != nullptr)
+ callback(c, event, index, userdata);
+ });
+
+ reinterpret_cast<PAMockContext*>(c)->addEventCallback(cppcb);
+}
+
+/* *******************************
+ * glib-mainloop.h
+ * *******************************/
+
+struct pa_glib_mainloop {
+ GMainContext * context;
+};
+
+struct pa_mainloop_api mock_mainloop = { 0 };
+
+pa_mainloop_api *
+pa_glib_mainloop_get_api (pa_glib_mainloop * g)
+{
+ return &mock_mainloop;
+}
+
+pa_glib_mainloop *
+pa_glib_mainloop_new (GMainContext * c)
+{
+ pa_glib_mainloop * loop = g_new0(pa_glib_mainloop, 1);
+
+ if (c == nullptr)
+ loop->context = g_main_context_default();
+ else
+ loop->context = c;
+
+ g_main_context_ref(loop->context);
+ return loop;
+}
+
+void
+pa_glib_mainloop_free (pa_glib_mainloop * g)
+{
+ g_main_context_unref(g->context);
+ g_free(g);
+}
+
+/* *******************************
+ * operation.h
+ * *******************************/
+
+void
+pa_operation_unref (pa_operation * operation)
+{
+ g_return_if_fail(G_IS_OBJECT(operation));
+ g_object_unref(G_OBJECT(operation));
+}
+
+/* *******************************
+ * proplist.h
+ * *******************************/
+
+pa_proplist *
+pa_proplist_new (void)
+{
+ return (pa_proplist *)g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+}
+
+void
+pa_proplist_free (pa_proplist * p)
+{
+ g_return_if_fail(p != nullptr);
+ g_hash_table_destroy((GHashTable *)p);
+}
+
+const char *
+pa_proplist_gets (pa_proplist * p, const char * key)
+{
+ g_return_val_if_fail(p != nullptr, nullptr);
+ g_return_val_if_fail(key != nullptr, nullptr);
+ return (const char *)g_hash_table_lookup((GHashTable *)p, key);
+}
+
+int
+pa_proplist_sets (pa_proplist *p, const char * key, const char * value)
+{
+ g_return_val_if_fail(p != nullptr, -1);
+ g_return_val_if_fail(key != nullptr, -1);
+ g_return_val_if_fail(value != nullptr, -1);
+
+ g_hash_table_insert((GHashTable *)p, g_strdup(key), g_strdup(value));
+ return 0;
+}
+
+/* *******************************
+ * error.h
+ * *******************************/
+
+const char *
+pa_strerror (int error)
+{
+ return "This is error text";
+}
+
+/* *******************************
+ * volume.h
+ * *******************************/
+
+pa_volume_t
+pa_sw_volume_from_dB (double f)
+{
+ double linear = pow(10.0, f / 20.0);
+
+ pa_volume_t calculated = lround(cbrt(linear) * PA_VOLUME_NORM);
+
+ if (G_UNLIKELY(calculated > PA_VOLUME_MAX)) {
+ return PA_VOLUME_MAX;
+ } else if (G_UNLIKELY(calculated < PA_VOLUME_MUTED)) {
+ return PA_VOLUME_MUTED;
+ } else {
+ return calculated;
+ }
+}
+
+pa_cvolume *
+pa_cvolume_init (pa_cvolume * cvol)
+{
+ g_return_val_if_fail(cvol != nullptr, nullptr);
+
+ cvol->channels = 0;
+
+ unsigned int i;
+ for (i = 0; i < PA_CHANNELS_MAX; i++)
+ cvol->values[i] = PA_VOLUME_INVALID;
+
+ return cvol;
+}
+
+pa_cvolume *
+pa_cvolume_set (pa_cvolume * cvol, unsigned channels, pa_volume_t volume)
+{
+ g_return_val_if_fail(cvol != nullptr, nullptr);
+ g_warn_if_fail(channels > 0);
+ g_return_val_if_fail(channels <= PA_CHANNELS_MAX, nullptr);
+
+ cvol->channels = channels;
+
+ unsigned int i;
+ for (i = 0; i < channels; i++) {
+ if (G_UNLIKELY(volume > PA_VOLUME_MAX)) {
+ cvol->values[i] = PA_VOLUME_MAX;
+ } else if (G_UNLIKELY(volume < PA_VOLUME_MUTED)) {
+ cvol->values[i] = PA_VOLUME_MUTED;
+ } else {
+ cvol->values[i] = volume;
+ }
+ }
+
+ return cvol;
+}
+
+pa_volume_t
+pa_cvolume_max (const pa_cvolume * cvol)
+{
+ g_return_val_if_fail(cvol != nullptr, PA_VOLUME_MUTED);
+ pa_volume_t max = PA_VOLUME_MUTED;
+
+ unsigned int i;
+ for (i = 0; i < cvol->channels; i++)
+ max = MAX(max, cvol->values[i]);
+
+ return max;
+}
+
+pa_cvolume *
+pa_cvolume_scale (pa_cvolume * cvol, pa_volume_t max)
+{
+ g_return_val_if_fail(cvol != nullptr, nullptr);
+
+ pa_volume_t originalmax = pa_cvolume_max(cvol);
+
+ if (originalmax <= PA_VOLUME_MUTED)
+ return pa_cvolume_set(cvol, cvol->channels, max);
+
+ unsigned int i;
+ for (i = 0; i < cvol->channels; i++) {
+ pa_volume_t calculated = (cvol->values[i] * max) / originalmax;
+
+ if (G_UNLIKELY(calculated > PA_VOLUME_MAX)) {
+ cvol->values[i] = PA_VOLUME_MAX;
+ } else if (G_UNLIKELY(calculated < PA_VOLUME_MUTED)) {
+ cvol->values[i] = PA_VOLUME_MUTED;
+ } else {
+ cvol->values[i] = calculated;
+ }
+ }
+
+ return cvol;
+}
+
diff --git a/tests/volume-control-test.cc b/tests/volume-control-test.cc
new file mode 100644
index 0000000..9970241
--- /dev/null
+++ b/tests/volume-control-test.cc
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+#include <libdbustest/dbus-test.h>
+
+extern "C" {
+#include "indicator-sound-service.h"
+}
+
+class VolumeControlTest : public ::testing::Test
+{
+
+ protected:
+ DbusTestService * service = NULL;
+ GDBusConnection * session = NULL;
+
+ virtual void SetUp() {
+ service = dbus_test_service_new(NULL);
+ 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);
+ }
+
+ virtual void TearDown() {
+ 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 * loop = static_cast<GMainLoop *>(user_data);
+ g_main_loop_quit(loop);
+ 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);
+ }
+};
+
+TEST_F(VolumeControlTest, BasicObject) {
+ VolumeControl * control = volume_control_new();
+
+ /* Setup the PA backend */
+ loop(100);
+
+ /* Ready */
+ EXPECT_TRUE(volume_control_get_ready(control));
+
+ g_clear_object(&control);
+}