Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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,
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include "nsIWifiListener.h"
#include "nsWifiMonitor.h"
#include "nsWifiAccessPoint.h"
#include "WifiScanner.h"
#include "nsCOMPtr.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "nsIObserverService.h"
#include "nsINetworkLinkService.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#if defined(XP_WIN) && defined(_M_IX86)
#  include <objbase.h>  // STDMETHODCALLTYPE
#endif
// Tests that wifi scanning happens on the right network change events,
// and that wifi-scan polling is operable on mobile networks.
using ::testing::AtLeast;
using ::testing::Cardinality;
using ::testing::Exactly;
using ::testing::MockFunction;
using ::testing::Sequence;
static mozilla::LazyLogModule gLog("TestWifiMonitor");
#define LOGI(x) MOZ_LOG(gLog, mozilla::LogLevel::Info, x)
#define LOGD(x) MOZ_LOG(gLog, mozilla::LogLevel::Debug, x)
namespace mozilla {
// Timeout if update not received from wifi scanner thread.
static const uint32_t kWifiScanTestResultTimeoutMs = 100;
static const uint32_t kTestWifiScanIntervalMs = 10;
// ID counter, used to make sure each call to GetAccessPointsFromWLAN
// returns "new" access points.
static int gCurrentId = 0;
static uint32_t gNumScanResults = 0;
struct LinkTypeMobility {
  const char* mLinkType;
  bool mIsMobile;
};
class MockWifiScanner : public WifiScanner {
 public:
  MOCK_METHOD(nsresult, GetAccessPointsFromWLAN,
              (nsTArray<RefPtr<nsIWifiAccessPoint>> & aAccessPoints),
              (override));
};
class MockWifiListener : public nsIWifiListener {
  virtual ~MockWifiListener() = default;
 public:
  NS_DECL_THREADSAFE_ISUPPORTS
#if defined(XP_WIN) && defined(_M_IX86)
  MOCK_METHOD(nsresult, OnChange,
              (const nsTArray<RefPtr<nsIWifiAccessPoint>>& accessPoints),
              (override, Calltype(STDMETHODCALLTYPE)));
  MOCK_METHOD(nsresult, OnError, (nsresult error),
              (override, Calltype(STDMETHODCALLTYPE)));
#else
  MOCK_METHOD(nsresult, OnChange,
              (const nsTArray<RefPtr<nsIWifiAccessPoint>>& accessPoints),
              (override));
  MOCK_METHOD(nsresult, OnError, (nsresult error), (override));
#endif
};
NS_IMPL_ISUPPORTS(MockWifiListener, nsIWifiListener)
class TestWifiMonitor : public ::testing::Test {
 public:
  TestWifiMonitor() {
    mObs = mozilla::services::GetObserverService();
    MOZ_RELEASE_ASSERT(mObs);
    nsresult rv;
    nsCOMPtr<nsINetworkLinkService> nls =
        do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    EXPECT_TRUE(nls);
    rv = nls->GetLinkType(&mOrigLinkType);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    rv = nls->GetIsLinkUp(&mOrigIsLinkUp);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    rv = nls->GetLinkStatusKnown(&mOrigLinkStatusKnown);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    // Reduce wifi-polling interval.  0 turns polling off.
    mOldScanInterval = Preferences::GetInt(WIFI_SCAN_INTERVAL_MS_PREF);
    Preferences::SetInt(WIFI_SCAN_INTERVAL_MS_PREF, kTestWifiScanIntervalMs);
  }
  ~TestWifiMonitor() {
    Preferences::SetInt(WIFI_SCAN_INTERVAL_MS_PREF, mOldScanInterval);
    // Restore network link type
    const char* linkType = nullptr;
    switch (mOrigLinkType) {
      case nsINetworkLinkService::LINK_TYPE_UNKNOWN:
        linkType = NS_NETWORK_LINK_TYPE_UNKNOWN;
        break;
      case nsINetworkLinkService::LINK_TYPE_ETHERNET:
        linkType = NS_NETWORK_LINK_TYPE_ETHERNET;
        break;
      case nsINetworkLinkService::LINK_TYPE_USB:
        linkType = NS_NETWORK_LINK_TYPE_USB;
        break;
      case nsINetworkLinkService::LINK_TYPE_WIFI:
        linkType = NS_NETWORK_LINK_TYPE_WIFI;
        break;
      case nsINetworkLinkService::LINK_TYPE_MOBILE:
        linkType = NS_NETWORK_LINK_TYPE_MOBILE;
        break;
      case nsINetworkLinkService::LINK_TYPE_WIMAX:
        linkType = NS_NETWORK_LINK_TYPE_WIMAX;
        break;
    }
    EXPECT_TRUE(linkType);
    mObs->NotifyObservers(nullptr, NS_NETWORK_LINK_TYPE_TOPIC,
                          NS_ConvertUTF8toUTF16(linkType).get());
    const char* linkStatus = nullptr;
    if (mOrigLinkStatusKnown) {
      if (mOrigIsLinkUp) {
        linkStatus = NS_NETWORK_LINK_DATA_UP;
      } else {
        linkStatus = NS_NETWORK_LINK_DATA_DOWN;
      }
    } else {
      linkStatus = NS_NETWORK_LINK_DATA_UNKNOWN;
    }
    EXPECT_TRUE(linkStatus);
    mObs->NotifyObservers(nullptr, NS_NETWORK_LINK_TOPIC,
                          NS_ConvertUTF8toUTF16(linkStatus).get());
  }
 protected:
  bool WaitForScanResults() {
    // Wait for kWifiScanTestResultTimeoutMs to allow async calls to complete.
    bool timedout = false;
    RefPtr<CancelableRunnable> timer = NS_NewCancelableRunnableFunction(
        "WaitForScanResults Timeout", [&] { timedout = true; });
    NS_DelayedDispatchToCurrentThread(do_AddRef(timer),
                                      kWifiScanTestResultTimeoutMs);
    mozilla::SpinEventLoopUntil("TestWifiMonitor::WaitForScanResults"_ns,
                                [&]() { return timedout; });
    timer->Cancel();
    return true;
  }
  void CreateObjects() {
    mWifiMonitor = MakeRefPtr<nsWifiMonitor>(MakeUnique<MockWifiScanner>());
    EXPECT_TRUE(!mWifiMonitor->IsPolling());
    // Start with ETHERNET network type to avoid always polling at test start.
    mObs->NotifyObservers(
        nullptr, NS_NETWORK_LINK_TYPE_TOPIC,
        NS_ConvertUTF8toUTF16(NS_NETWORK_LINK_TYPE_ETHERNET).get());
    mWifiListener = new MockWifiListener();
    LOGI(("monitor: %p | scanner: %p | listener: %p", mWifiMonitor.get(),
          mWifiMonitor->mWifiScanner.get(), mWifiListener.get()));
  }
  void DestroyObjects() {
    ::testing::Mock::VerifyAndClearExpectations(
        mWifiMonitor->mWifiScanner.get());
    ::testing::Mock::VerifyAndClearExpectations(mWifiListener.get());
    // Manually disconnect observers so that the monitor can be destroyed.
    // In the browser, this would be done on xpcom-shutdown but that is sent
    // after the tests run, which is too late to avoid a gtest memory-leak
    // error.
    mWifiMonitor->Close();
    mWifiMonitor = nullptr;
    mWifiListener = nullptr;
    gCurrentId = 0;
  }
  void StartWatching(bool aRequestPolling) {
    LOGD(("StartWatching | aRequestPolling: %s | nScanResults: %u",
          aRequestPolling ? "true" : "false", gNumScanResults));
    EXPECT_TRUE(NS_SUCCEEDED(
        mWifiMonitor->StartWatching(mWifiListener, aRequestPolling)));
    WaitForScanResults();
  }
  void NotifyOfNetworkEvent(const char* aTopic, const char16_t* aData) {
    LOGD(("NotifyOfNetworkEvent: (%s, %s) | nScanResults: %u", aTopic,
          NS_ConvertUTF16toUTF8(aData).get(), gNumScanResults));
    EXPECT_TRUE(NS_SUCCEEDED(mObs->NotifyObservers(nullptr, aTopic, aData)));
    WaitForScanResults();
  }
  void StopWatching() {
    LOGD(("StopWatching | nScanResults: %u", gNumScanResults));
    EXPECT_TRUE(NS_SUCCEEDED(mWifiMonitor->StopWatching(mWifiListener)));
    WaitForScanResults();
  }
  struct MockCallSequences {
    Sequence mGetAccessPointsSeq;
    Sequence mOnChangeSeq;
    Sequence mOnErrorSeq;
  };
  void AddMockObjectChecks(const Cardinality& aScanCardinality,
                           MockCallSequences& aSeqs) {
    // Only add WillRepeatedly handler if scans is more than 0, to avoid a
    // VERY LOUD gtest warning.
    if (aScanCardinality.IsSaturatedByCallCount(0)) {
      EXPECT_CALL(
          *static_cast<MockWifiScanner*>(mWifiMonitor->mWifiScanner.get()),
          GetAccessPointsFromWLAN)
          .Times(aScanCardinality)
          .InSequence(aSeqs.mGetAccessPointsSeq);
      EXPECT_CALL(*mWifiListener, OnChange)
          .Times(aScanCardinality)
          .InSequence(aSeqs.mOnChangeSeq);
    } else {
      EXPECT_CALL(
          *static_cast<MockWifiScanner*>(mWifiMonitor->mWifiScanner.get()),
          GetAccessPointsFromWLAN)
          .Times(aScanCardinality)
          .InSequence(aSeqs.mGetAccessPointsSeq)
          .WillRepeatedly(
              [](nsTArray<RefPtr<nsIWifiAccessPoint>>& aAccessPoints) {
                EXPECT_TRUE(!NS_IsMainThread());
                EXPECT_TRUE(aAccessPoints.IsEmpty());
                nsWifiAccessPoint* ap = new nsWifiAccessPoint();
                // Signal will be unique so we won't match the prior access
                // point list.
                ap->mSignal = gCurrentId++;
                aAccessPoints.AppendElement(RefPtr(ap));
                return NS_OK;
              });
      EXPECT_CALL(*mWifiListener, OnChange)
          .Times(aScanCardinality)
          .InSequence(aSeqs.mOnChangeSeq)
          .WillRepeatedly(
              [](const nsTArray<RefPtr<nsIWifiAccessPoint>>& aAccessPoints) {
                EXPECT_TRUE(NS_IsMainThread());
                EXPECT_EQ(aAccessPoints.Length(), 1u);
                ++gNumScanResults;
                return NS_OK;
              });
    }
    EXPECT_CALL(*mWifiListener, OnError).Times(0).InSequence(aSeqs.mOnErrorSeq);
  }
  void AddStartWatchingCheck(bool aShouldPoll, MockCallSequences& aSeqs) {
    AddMockObjectChecks(aShouldPoll ? AtLeast(1) : Exactly(1), aSeqs);
  }
  void AddNetworkEventCheck(const Cardinality& aScanCardinality,
                            MockCallSequences& aSeqs) {
    AddMockObjectChecks(aScanCardinality, aSeqs);
  }
  void AddStopWatchingCheck(bool aShouldPoll, MockCallSequences& aSeqs) {
    // When polling, we may get stray scan + OnChange calls asynchronously
    // before stopping.  We may also get scan calls after stopping.
    // We check that the calls actually stopped in ConfirmStoppedCheck.
    AddMockObjectChecks(aShouldPoll ? AtLeast(0) : Exactly(0), aSeqs);
  }
  void AddConfirmStoppedCheck(MockCallSequences& aSeqs) {
    AddMockObjectChecks(Exactly(0), aSeqs);
  }
  // A Checkpoint is just a mocked function taking an int.  It will serve
  // as a temporal barrier that requires all expectations before it to be
  // satisfied and retired (meaning they won't be used in matches anymore).
  class Checkpoint {
   public:
    void Check(uint32_t aId, MockCallSequences& aSeqs) {
      EXPECT_CALL(mFn, Call(aId))
          .InSequence(aSeqs.mGetAccessPointsSeq, aSeqs.mOnChangeSeq,
                      aSeqs.mOnErrorSeq);
    }
    void Reach(uint32_t aId) { mFn.Call(aId); }
   private:
    MockFunction<void(uint32_t)> mFn;
  };
  // A single test is StartWatching, NotifyOfNetworkEvent, and StopWatching.
  void RunSingleTest(bool aRequestPolling, bool aShouldPoll,
                     const Cardinality& aScanCardinality, const char* aTopic,
                     const char16_t* aData) {
    LOGI(("RunSingleTest: <%s, %s> | requestPolling: %s | shouldPoll: %s",
          aTopic, NS_ConvertUTF16toUTF8(aData).get(),
          aRequestPolling ? "true" : "false", aShouldPoll ? "true" : "false"));
    MOZ_RELEASE_ASSERT(aShouldPoll || !aRequestPolling);
    CreateObjects();
    Checkpoint checkpoint;
    {
      // gmock expectations are asynchronous by default.  Sequence objects
      // are used here to require that expectations occur in the specified
      // (partial) order.
      MockCallSequences seqs;
      AddStartWatchingCheck(aShouldPoll, seqs);
      checkpoint.Check(1, seqs);
      AddNetworkEventCheck(aScanCardinality, seqs);
      checkpoint.Check(2, seqs);
      AddStopWatchingCheck(aShouldPoll, seqs);
      checkpoint.Check(3, seqs);
      AddConfirmStoppedCheck(seqs);
    }
    // Now run the test on the mock objects.
    StartWatching(aRequestPolling);
    checkpoint.Reach(1);
    EXPECT_EQ(mWifiMonitor->IsPolling(), aRequestPolling);
    NotifyOfNetworkEvent(aTopic, aData);
    checkpoint.Reach(2);
    EXPECT_EQ(mWifiMonitor->IsPolling(), aShouldPoll);
    StopWatching();
    checkpoint.Reach(3);
    EXPECT_TRUE(!mWifiMonitor->IsPolling());
    // Wait for extraneous calls as a way to confirm it has stopped.
    WaitForScanResults();
    DestroyObjects();
  }
  void CheckMessages(bool aRequestPolling) {
    // NS_NETWORK_LINK_TOPIC messages should cause a new scan.
    const char* kLinkTopicDatas[] = {
        NS_NETWORK_LINK_DATA_UP, NS_NETWORK_LINK_DATA_DOWN,
        NS_NETWORK_LINK_DATA_CHANGED, NS_NETWORK_LINK_DATA_UNKNOWN};
    for (const auto& data : kLinkTopicDatas) {
      RunSingleTest(aRequestPolling, aRequestPolling,
                    aRequestPolling ? AtLeast(2) : Exactly(1),
                    NS_NETWORK_LINK_TOPIC, NS_ConvertUTF8toUTF16(data).get());
    }
    // NS_NETWORK_LINK_TYPE_TOPIC should cause wifi scan polling iff the topic
    // says we have switched to a mobile network (LINK_TYPE_MOBILE or
    // LINK_TYPE_WIMAX) or we are polling the wifi-scanner (aShouldPoll).
    const LinkTypeMobility kLinkTypeTopicDatas[] = {
        {NS_NETWORK_LINK_TYPE_UNKNOWN, true /* mIsMobile */},
        {NS_NETWORK_LINK_TYPE_ETHERNET, false},
        {NS_NETWORK_LINK_TYPE_USB, false},
        {NS_NETWORK_LINK_TYPE_WIFI, false},
        {NS_NETWORK_LINK_TYPE_WIMAX, true},
        {NS_NETWORK_LINK_TYPE_MOBILE, true}};
    for (const auto& data : kLinkTypeTopicDatas) {
      bool shouldPoll = (aRequestPolling || data.mIsMobile);
      RunSingleTest(aRequestPolling, shouldPoll,
                    shouldPoll ? AtLeast(2) : Exactly(0),
                    NS_NETWORK_LINK_TYPE_TOPIC,
                    NS_ConvertUTF8toUTF16(data.mLinkType).get());
    }
  }
  RefPtr<nsWifiMonitor> mWifiMonitor;
  nsCOMPtr<nsIObserverService> mObs;
  RefPtr<MockWifiListener> mWifiListener;
  int mOldScanInterval;
  uint32_t mOrigLinkType = 0;
  bool mOrigIsLinkUp = false;
  bool mOrigLinkStatusKnown = false;
};
TEST_F(TestWifiMonitor, WifiScanNoPolling) { CheckMessages(false); }
TEST_F(TestWifiMonitor, WifiScanPolling) { CheckMessages(true); }
}  // namespace mozilla