/* -*- 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 // 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> & 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>& accessPoints), (override, Calltype(STDMETHODCALLTYPE))); MOCK_METHOD(nsresult, OnError, (nsresult error), (override, Calltype(STDMETHODCALLTYPE))); #else MOCK_METHOD(nsresult, OnChange, (const nsTArray>& 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 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 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(MakeUnique()); 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(mWifiMonitor->mWifiScanner.get()), GetAccessPointsFromWLAN) .Times(aScanCardinality) .InSequence(aSeqs.mGetAccessPointsSeq); EXPECT_CALL(*mWifiListener, OnChange) .Times(aScanCardinality) .InSequence(aSeqs.mOnChangeSeq); } else { EXPECT_CALL( *static_cast(mWifiMonitor->mWifiScanner.get()), GetAccessPointsFromWLAN) .Times(aScanCardinality) .InSequence(aSeqs.mGetAccessPointsSeq) .WillRepeatedly( [](nsTArray>& 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>& 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 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 mWifiMonitor; nsCOMPtr mObs; RefPtr 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