Bug 1404977 - Part 16 - Unit test CubebDeviceEnumerator. r?pehrsons draft
authorPaul Adenot <paul@paul.cx>
Fri, 22 Jun 2018 11:55:31 +0200
changeset 824657 ff8e7e4cda229cbd22ac4939db9b0da326a9e508
parent 824656 9cab933a4ba12b7c5f8f92af77a757d1fe92acc1
child 824658 56c51d991df98b4ec8b41cebea02133b19e30bf4
push id117965
push userpaul@paul.cx
push dateTue, 31 Jul 2018 14:52:35 +0000
reviewerspehrsons
bugs1404977
milestone63.0a1
Bug 1404977 - Part 16 - Unit test CubebDeviceEnumerator. r?pehrsons This is done by implementing a fake cubeb backend that implements the subset of operations we need, while offering an API to be able to control what this backend is doing. Because we're reimplementing the private cubeb API, it is necessary to copy part of a cubeb internal header, and mimick exactly how the vtable mechanism to do the dynamic dispatch to the diffferent backends in cubeb works. This is not ideal but works. When the cubeb API functions are called (from deep in the Gecko process), we re-bind the call to the mock cubeb backend object and behave exactly like a normal backend (calling various callbacks and returning fake objects). Finally, we inject this mock cubeb backend to the running Gecko process (in lieu of the real one that would have been picked) by setting the global sCubebBackend variable via a private API exposed only in the test in CubebUtils.h. MozReview-Commit-ID: 8ZbJhl7pZ2t
dom/media/gtest/TestAudioDeviceEnumerator.cpp
dom/media/gtest/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/media/gtest/TestAudioDeviceEnumerator.cpp
@@ -0,0 +1,554 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Attributes.h"
+#include "nsTArray.h"
+#define ENABLE_SET_CUBEB_BACKEND 1
+#include "CubebUtils.h"
+#include "MediaEngineWebRTC.h"
+
+using namespace mozilla;
+
+const bool DEBUG_PRINTS = false;
+
+// Keep those and the struct definition in sync with cubeb.h and
+// cubeb-internal.h
+void
+cubeb_mock_destroy(cubeb* context);
+static int
+cubeb_mock_enumerate_devices(cubeb* context,
+                             cubeb_device_type type,
+                             cubeb_device_collection* out);
+
+static int
+cubeb_mock_device_collection_destroy(cubeb* context,
+                                     cubeb_device_collection* collection);
+
+static int
+cubeb_mock_register_device_collection_changed(
+  cubeb* context,
+  cubeb_device_type devtype,
+  cubeb_device_collection_changed_callback callback,
+  void* user_ptr);
+
+struct cubeb_ops
+{
+  int (*init)(cubeb** context, char const* context_name);
+  char const* (*get_backend_id)(cubeb* context);
+  int (*get_max_channel_count)(cubeb* context, uint32_t* max_channels);
+  int (*get_min_latency)(cubeb* context,
+                         cubeb_stream_params params,
+                         uint32_t* latency_ms);
+  int (*get_preferred_sample_rate)(cubeb* context, uint32_t* rate);
+  int (*enumerate_devices)(cubeb* context,
+                           cubeb_device_type type,
+                           cubeb_device_collection* collection);
+  int (*device_collection_destroy)(cubeb* context,
+                                   cubeb_device_collection* collection);
+  void (*destroy)(cubeb* context);
+  int (*stream_init)(cubeb* context,
+                     cubeb_stream** stream,
+                     char const* stream_name,
+                     cubeb_devid input_device,
+                     cubeb_stream_params* input_stream_params,
+                     cubeb_devid output_device,
+                     cubeb_stream_params* output_stream_params,
+                     unsigned int latency,
+                     cubeb_data_callback data_callback,
+                     cubeb_state_callback state_callback,
+                     void* user_ptr);
+  void (*stream_destroy)(cubeb_stream* stream);
+  int (*stream_start)(cubeb_stream* stream);
+  int (*stream_stop)(cubeb_stream* stream);
+  int (*stream_reset_default_device)(cubeb_stream* stream);
+  int (*stream_get_position)(cubeb_stream* stream, uint64_t* position);
+  int (*stream_get_latency)(cubeb_stream* stream, uint32_t* latency);
+  int (*stream_set_volume)(cubeb_stream* stream, float volumes);
+  int (*stream_set_panning)(cubeb_stream* stream, float panning);
+  int (*stream_get_current_device)(cubeb_stream* stream,
+                                   cubeb_device** const device);
+  int (*stream_device_destroy)(cubeb_stream* stream, cubeb_device* device);
+  int (*stream_register_device_changed_callback)(
+    cubeb_stream* stream,
+    cubeb_device_changed_callback device_changed_callback);
+  int (*register_device_collection_changed)(
+    cubeb* context,
+    cubeb_device_type devtype,
+    cubeb_device_collection_changed_callback callback,
+    void* user_ptr);
+};
+
+// Mock cubeb impl, only supports device enumeration for now.
+cubeb_ops const mock_ops = {
+  /*.init =*/NULL,
+  /*.get_backend_id =*/NULL,
+  /*.get_max_channel_count =*/NULL,
+  /*.get_min_latency =*/NULL,
+  /*.get_preferred_sample_rate =*/NULL,
+  /*.enumerate_devices =*/cubeb_mock_enumerate_devices,
+  /*.device_collection_destroy =*/cubeb_mock_device_collection_destroy,
+  /*.destroy =*/cubeb_mock_destroy,
+  /*.stream_init =*/NULL,
+  /*.stream_destroy =*/NULL,
+  /*.stream_start =*/NULL,
+  /*.stream_stop =*/NULL,
+  /*.stream_reset_default_device =*/NULL,
+  /*.stream_get_position =*/NULL,
+  /*.stream_get_latency =*/NULL,
+  /*.stream_set_volume =*/NULL,
+  /*.stream_set_panning =*/NULL,
+  /*.stream_get_current_device =*/NULL,
+  /*.stream_device_destroy =*/NULL,
+  /*.stream_register_device_changed_callback =*/NULL,
+  /*.register_device_collection_changed =*/
+  cubeb_mock_register_device_collection_changed
+};
+
+// This class has two facets: it is both a fake cubeb backend that is intended
+// to be used for testing, and passed to Gecko code that expects a normal
+// backend, but is also controllable by the test code to decide what the backend
+// should do, depending on what is being tested.
+class MockCubeb
+{
+public:
+  MockCubeb()
+    : ops(&mock_ops)
+    , mDeviceCollectionChangeCallback(nullptr)
+    , mDeviceCollectionChangeType(CUBEB_DEVICE_TYPE_UNKNOWN)
+    , mDeviceCollectionChangeUserPtr(nullptr)
+    , mSupportsDeviceCollectionChangedCallback(true)
+  {
+  }
+  // Cubeb backend implementation
+  // This allows passing this class as a cubeb* instance.
+  cubeb* AsCubebContext() { return reinterpret_cast<cubeb*>(this); }
+  // Fill in the collection parameter with all devices of aType.
+  int EnumerateDevices(cubeb_device_type aType,
+                       cubeb_device_collection* collection)
+  {
+    size_t count = 0;
+    if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+      count += mInputDevices.Length();
+    }
+    if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+      count += mOutputDevices.Length();
+    }
+    collection->device = new cubeb_device_info[count];
+    collection->count = count;
+
+    uint32_t collection_index = 0;
+    if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+      for (auto& device : mInputDevices) {
+        collection->device[collection_index] = device;
+        collection_index++;
+      }
+    }
+    if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+      for (auto& device : mOutputDevices) {
+        collection->device[collection_index] = device;
+        collection_index++;
+      }
+    }
+
+    return CUBEB_OK;
+  }
+
+  // For a given device type, add a callback, called with a user pointer, when
+  // the device collection for this backend changes (i.e. a device has been
+  // removed or added).
+  int RegisterDeviceCollectionChangeCallback(
+    cubeb_device_type aDevType,
+    cubeb_device_collection_changed_callback aCallback,
+    void* aUserPtr)
+  {
+    if (!mSupportsDeviceCollectionChangedCallback) {
+      return CUBEB_ERROR;
+    }
+
+    mDeviceCollectionChangeType = aDevType;
+    mDeviceCollectionChangeCallback = aCallback;
+    mDeviceCollectionChangeUserPtr = aUserPtr;
+
+    return CUBEB_OK;
+  }
+
+  // Control API
+
+  // Add an input or output device to this backend. This calls the device
+  // collection invalidation callback if needed.
+  void AddDevice(cubeb_device_info aDevice)
+  {
+    bool needToCall = false;
+
+    if (aDevice.type == CUBEB_DEVICE_TYPE_INPUT) {
+      mInputDevices.AppendElement(aDevice);
+    } else if (aDevice.type == CUBEB_DEVICE_TYPE_OUTPUT) {
+      mOutputDevices.AppendElement(aDevice);
+    } else {
+      MOZ_CRASH("bad device type when adding a device in mock cubeb backend");
+    }
+
+    bool isInput = aDevice.type & CUBEB_DEVICE_TYPE_INPUT;
+
+    needToCall |=
+      isInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_INPUT;
+    needToCall |=
+      !isInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_OUTPUT;
+
+    if (needToCall && mDeviceCollectionChangeCallback) {
+      mDeviceCollectionChangeCallback(AsCubebContext(),
+                                      mDeviceCollectionChangeUserPtr);
+    }
+  }
+  // Remove a specific input or output device to this backend, returns true if
+  // a device was removed. This calls the device collection invalidation
+  // callback if needed.
+  bool RemoveDevice(cubeb_devid aId)
+  {
+    bool foundInput = false;
+    bool foundOutput = false;
+    mInputDevices.RemoveElementsBy(
+      [aId, &foundInput](cubeb_device_info& aDeviceInfo) {
+        bool foundThisTime = aDeviceInfo.devid == aId;
+        foundInput |= foundThisTime;
+        return foundThisTime;
+      });
+    mOutputDevices.RemoveElementsBy(
+      [aId, &foundOutput](cubeb_device_info& aDeviceInfo) {
+        bool foundThisTime = aDeviceInfo.devid == aId;
+        foundOutput |= foundThisTime;
+        return foundThisTime;
+      });
+
+    bool needToCall = false;
+
+    needToCall |=
+      foundInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_INPUT;
+    needToCall |=
+      foundOutput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_OUTPUT;
+
+    if (needToCall && mDeviceCollectionChangeCallback) {
+      mDeviceCollectionChangeCallback(AsCubebContext(),
+                                      mDeviceCollectionChangeUserPtr);
+    }
+
+    // If the device removed was a default device, set another device as the
+    // default, if there are still devices available.
+    bool foundDefault = false;
+    for (uint32_t i = 0; i < mInputDevices.Length(); i++) {
+      foundDefault |= mInputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+    }
+
+    if (!foundDefault) {
+      if (!mInputDevices.IsEmpty()) {
+        mInputDevices[mInputDevices.Length() - 1].preferred =
+          CUBEB_DEVICE_PREF_ALL;
+      }
+    }
+
+    foundDefault = false;
+    for (uint32_t i = 0; i < mOutputDevices.Length(); i++) {
+      foundDefault |= mOutputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+    }
+
+    if (!foundDefault) {
+      if (!mOutputDevices.IsEmpty()) {
+        mOutputDevices[mOutputDevices.Length() - 1].preferred =
+          CUBEB_DEVICE_PREF_ALL;
+      }
+    }
+
+    return foundInput | foundOutput;
+  }
+  // Remove all input or output devices from this backend, without calling the
+  // callback. This is meant to clean up in between tests.
+  void ClearDevices(cubeb_device_type aType)
+  {
+    mInputDevices.Clear();
+    mOutputDevices.Clear();
+  }
+
+  // This allows simulating a backend that does not support setting a device
+  // collection invalidation callback, to be able to test the fallback path.
+  void SetSupportDeviceChangeCallback(bool aSupports)
+  {
+    mSupportsDeviceCollectionChangedCallback = aSupports;
+  }
+
+private:
+  // This needs to have the exact same memory layout as a real cubeb backend.
+  // It's very important for this `ops` member to be the very first member of
+  // the class, and to not have any virtual members (to avoid having a vtable).
+  const cubeb_ops* ops;
+  // The callback to call when the device list has been changed.
+  cubeb_device_collection_changed_callback mDeviceCollectionChangeCallback;
+  // For which device type to call the callback.
+  cubeb_device_type mDeviceCollectionChangeType;
+  // The pointer to pass in the callback.
+  void* mDeviceCollectionChangeUserPtr;
+  // Whether or not this backed supports device collection change notification
+  // via a system callback. If not, Gecko is expected to re-query the list every
+  // time.
+  bool mSupportsDeviceCollectionChangedCallback;
+  // Our input and output devices.
+  nsTArray<cubeb_device_info> mInputDevices;
+  nsTArray<cubeb_device_info> mOutputDevices;
+};
+
+void
+cubeb_mock_destroy(cubeb* context)
+{
+  delete reinterpret_cast<MockCubeb*>(context);
+}
+
+static int
+cubeb_mock_enumerate_devices(cubeb* context,
+                             cubeb_device_type type,
+                             cubeb_device_collection* out)
+{
+  MockCubeb* mock = reinterpret_cast<MockCubeb*>(context);
+  return mock->EnumerateDevices(type, out);
+}
+
+int
+cubeb_mock_device_collection_destroy(cubeb* context,
+                                     cubeb_device_collection* collection)
+{
+  delete[] collection->device;
+  return CUBEB_OK;
+}
+
+int
+cubeb_mock_register_device_collection_changed(
+  cubeb* context,
+  cubeb_device_type devtype,
+  cubeb_device_collection_changed_callback callback,
+  void* user_ptr)
+{
+  MockCubeb* mock = reinterpret_cast<MockCubeb*>(context);
+  return mock->RegisterDeviceCollectionChangeCallback(
+    devtype, callback, user_ptr);
+  return CUBEB_OK;
+}
+
+void
+PrintDevice(cubeb_device_info aInfo)
+{
+  printf("id: %zu\n"
+         "device_id: %s\n"
+         "friendly_name: %s\n"
+         "group_id: %s\n"
+         "vendor_name: %s\n"
+         "type: %d\n"
+         "state: %d\n"
+         "preferred: %d\n"
+         "format: %d\n"
+         "default_format: %d\n"
+         "max_channels: %d\n"
+         "default_rate: %d\n"
+         "max_rate: %d\n"
+         "min_rate: %d\n"
+         "latency_lo: %d\n"
+         "latency_hi: %d\n",
+         reinterpret_cast<uintptr_t>(aInfo.devid),
+         aInfo.device_id,
+         aInfo.friendly_name,
+         aInfo.group_id,
+         aInfo.vendor_name,
+         aInfo.type,
+         aInfo.state,
+         aInfo.preferred,
+         aInfo.format,
+         aInfo.default_format,
+         aInfo.max_channels,
+         aInfo.default_rate,
+         aInfo.max_rate,
+         aInfo.min_rate,
+         aInfo.latency_lo,
+         aInfo.latency_hi);
+}
+
+void
+PrintDevice(AudioDeviceInfo* aInfo)
+{
+  cubeb_devid id;
+  nsString name;
+  nsString groupid;
+  nsString vendor;
+  uint16_t type;
+  uint16_t state;
+  uint16_t preferred;
+  uint16_t supportedFormat;
+  uint16_t defaultFormat;
+  uint32_t maxChannels;
+  uint32_t defaultRate;
+  uint32_t maxRate;
+  uint32_t minRate;
+  uint32_t maxLatency;
+  uint32_t minLatency;
+
+  id = aInfo->DeviceID();
+  aInfo->GetName(name);
+  aInfo->GetGroupId(groupid);
+  aInfo->GetVendor(vendor);
+  aInfo->GetType(&type);
+  aInfo->GetState(&state);
+  aInfo->GetPreferred(&preferred);
+  aInfo->GetSupportedFormat(&supportedFormat);
+  aInfo->GetDefaultFormat(&defaultFormat);
+  aInfo->GetMaxChannels(&maxChannels);
+  aInfo->GetDefaultRate(&defaultRate);
+  aInfo->GetMaxRate(&maxRate);
+  aInfo->GetMinRate(&minRate);
+  aInfo->GetMinLatency(&minLatency);
+  aInfo->GetMaxLatency(&maxLatency);
+
+  printf("device id: %zu\n"
+         "friendly_name: %s\n"
+         "group_id: %s\n"
+         "vendor_name: %s\n"
+         "type: %d\n"
+         "state: %d\n"
+         "preferred: %d\n"
+         "format: %d\n"
+         "default_format: %d\n"
+         "max_channels: %d\n"
+         "default_rate: %d\n"
+         "max_rate: %d\n"
+         "min_rate: %d\n"
+         "latency_lo: %d\n"
+         "latency_hi: %d\n",
+         reinterpret_cast<uintptr_t>(id),
+         NS_LossyConvertUTF16toASCII(name).get(),
+         NS_LossyConvertUTF16toASCII(groupid).get(),
+         NS_LossyConvertUTF16toASCII(vendor).get(),
+         type,
+         state,
+         preferred,
+         supportedFormat,
+         defaultFormat,
+         maxChannels,
+         defaultRate,
+         maxRate,
+         minRate,
+         minLatency,
+         maxLatency);
+}
+
+cubeb_device_info
+InputDeviceTemplate(cubeb_devid aId)
+{
+  // A fake input device
+  cubeb_device_info device;
+  device.devid = aId;
+  device.device_id = "nice name";
+  device.friendly_name = "an even nicer name";
+  device.group_id = "the physical device";
+  device.vendor_name = "mozilla";
+  device.type = CUBEB_DEVICE_TYPE_INPUT;
+  device.state = CUBEB_DEVICE_STATE_ENABLED;
+  device.preferred = CUBEB_DEVICE_PREF_NONE;
+  device.format = CUBEB_DEVICE_FMT_F32NE;
+  device.default_format = CUBEB_DEVICE_FMT_F32NE;
+  device.max_channels = 2;
+  device.default_rate = 44100;
+  device.max_rate = 44100;
+  device.min_rate = 16000;
+  device.latency_lo = 256;
+  device.latency_hi = 1024;
+
+  return device;
+}
+
+enum DeviceOperation
+{
+  ADD,
+  REMOVE
+};
+
+void
+TestEnumeration(MockCubeb* aMock,
+                uint32_t aExpectedDeviceCount,
+                DeviceOperation aOperation)
+{
+  CubebDeviceEnumerator enumerator;
+
+  nsTArray<RefPtr<AudioDeviceInfo>> inputDevices;
+
+  enumerator.EnumerateAudioInputDevices(inputDevices);
+
+  EXPECT_EQ(inputDevices.Length(), aExpectedDeviceCount)
+    << "Device count is correct when enumerating";
+
+  if (DEBUG_PRINTS) {
+    for (uint32_t i = 0; i < inputDevices.Length(); i++) {
+      printf("=== Before removal\n");
+      PrintDevice(inputDevices[i]);
+    }
+  }
+
+  if (aOperation == DeviceOperation::REMOVE) {
+    aMock->RemoveDevice(reinterpret_cast<cubeb_devid>(1));
+  } else {
+    aMock->AddDevice(InputDeviceTemplate(reinterpret_cast<cubeb_devid>(123)));
+  }
+
+  enumerator.EnumerateAudioInputDevices(inputDevices);
+
+  uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE
+                                      ? aExpectedDeviceCount - 1
+                                      : aExpectedDeviceCount + 1;
+
+  EXPECT_EQ(inputDevices.Length(), newExpectedDeviceCount)
+    << "Device count is correct when enumerating after operation";
+
+  if (DEBUG_PRINTS) {
+    for (uint32_t i = 0; i < inputDevices.Length(); i++) {
+      printf("=== After removal\n");
+      PrintDevice(inputDevices[i]);
+    }
+  }
+}
+
+TEST(CubebDeviceEnumerator, EnumerateSimple)
+{
+  // It looks like we're leaking this object, but in fact it will be freed by
+  // gecko sometime later: `cubeb_destroy` is called when layout statics are
+  // shutdown and we cast back to a MockCubeb* and call the dtor.
+  MockCubeb* mock = new MockCubeb();
+  mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+  // We want to test whether CubebDeviceEnumerator works with and without a
+  // backend that can notify of a device collection change via callback.
+  // Additionally, we're testing that both adding and removing a device
+  // invalidates the list correctly.
+  bool supportsDeviceChangeCallback[2] = { true, false };
+  DeviceOperation operations[2] = { DeviceOperation::ADD,
+                                    DeviceOperation::REMOVE };
+
+  for (DeviceOperation op : operations) {
+    for (bool supports : supportsDeviceChangeCallback) {
+      mock->ClearDevices(CUBEB_DEVICE_TYPE_INPUT);
+      // Add a few input devices (almost all the same but it does not really
+      // matter as long as they have distinct IDs and only one is the default
+      // devices)
+      uint32_t device_count = 4;
+      for (uintptr_t i = 0; i < device_count; i++) {
+        cubeb_device_info device =
+          InputDeviceTemplate(reinterpret_cast<void*>(i + 1));
+        // Make it so that the last device is the default input device.
+        if (i == device_count - 1) {
+          device.preferred = CUBEB_DEVICE_PREF_ALL;
+        }
+        mock->AddDevice(device);
+      }
+
+      mock->SetSupportDeviceChangeCallback(supports);
+      TestEnumeration(mock, device_count, op);
+    }
+  }
+}
--- a/dom/media/gtest/moz.build
+++ b/dom/media/gtest/moz.build
@@ -1,18 +1,27 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+include('/media/webrtc/webrtc.mozbuild')
+
+
+LOCAL_INCLUDES += [
+    '/media/webrtc/signaling/src/common',
+    '/media/webrtc/trunk'
+]
+
 UNIFIED_SOURCES += [
     'MockMediaResource.cpp',
     'TestAudioBuffers.cpp',
     'TestAudioCompactor.cpp',
+    'TestAudioDeviceEnumerator.cpp',
     'TestAudioMixer.cpp',
     'TestAudioPacketizer.cpp',
     'TestAudioSegment.cpp',
     'TestAudioTrackEncoder.cpp',
     'TestBitWriter.cpp',
     'TestBlankVideoDataCreator.cpp',
     'TestCDMStorage.cpp',
     'TestDataMutex.cpp',
@@ -73,9 +82,9 @@ LOCAL_INCLUDES += [
     '/dom/media/platforms/agnostic',
     '/security/certverifier',
     '/security/pkix/include',
 ]
 
 FINAL_LIBRARY = 'xul-gtest'
 
 if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
-    CXXFLAGS += ['-Wno-error=shadow']
+    CXXFLAGS += ['-Wno-error=shadow', '-Wno-unused-private-field']