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,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#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_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_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