[BOT.3] Add unit test for polling network stats in the coordinator
Verify that the coordinator could fetch tether stats from BPF maps and
report the network stats to the service.
Bug: 150736748
Test: atest BpfCoordinatorTest
Original-Change: https://android-review.googlesource.com/1305574
Merged-In: Ib1756159a2047c5db7d31359b0f288f840bd1bb1
Change-Id: Ib1756159a2047c5db7d31359b0f288f840bd1bb1
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 0092eb7..aded6cf 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -41,6 +41,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* This coordinator is responsible for providing BPF offload relevant functionality.
* - Get tethering stats.
@@ -49,10 +51,11 @@
*/
public class BpfCoordinator {
private static final String TAG = BpfCoordinator.class.getSimpleName();
- // TODO: Make it customizable.
- private static final int DEFAULT_PERFORM_POLL_INTERVAL_MS = 5000;
+ @VisibleForTesting
+ static final int DEFAULT_PERFORM_POLL_INTERVAL_MS = 5000; // TODO: Make it customizable.
- private enum StatsType {
+ @VisibleForTesting
+ enum StatsType {
STATS_PER_IFACE,
STATS_PER_UID,
}
@@ -86,6 +89,7 @@
maybeSchedulePollingStats();
};
+ @VisibleForTesting
static class Dependencies {
int getPerformPollInterval() {
// TODO: Consider make this configurable.
@@ -169,7 +173,8 @@
* A BPF tethering stats provider to provide network statistics to the system.
* Note that this class's data may only be accessed on the handler thread.
*/
- private class BpfTetherStatsProvider extends NetworkStatsProvider {
+ @VisibleForTesting
+ class BpfTetherStatsProvider extends NetworkStatsProvider {
// The offloaded traffic statistics per interface that has not been reported since the
// last call to pushTetherStats. Only the interfaces that were ever tethering upstreams
// and has pending tether stats delta are included in this NetworkStats object.
@@ -193,7 +198,8 @@
// no-op
}
- private void pushTetherStats() {
+ @VisibleForTesting
+ void pushTetherStats() {
try {
// The token is not used for now. See b/153606961.
notifyStatsUpdated(0 /* token */, mIfaceStats, mUidStats);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
new file mode 100644
index 0000000..b029b43
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStats.UID_TETHERING;
+
+import static com.android.networkstack.tethering.BpfCoordinator
+ .DEFAULT_PERFORM_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+
+import static junit.framework.Assert.assertNotNull;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.usage.NetworkStatsManager;
+import android.net.INetd;
+import android.net.NetworkStats;
+import android.net.TetherStatsParcel;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BpfCoordinatorTest {
+ @Mock private NetworkStatsManager mStatsManager;
+ @Mock private INetd mNetd;
+ // Late init since methods must be called by the thread that created this object.
+ private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
+ private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
+ private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
+ ArgumentCaptor.forClass(ArrayList.class);
+ private final TestLooper mTestLooper = new TestLooper();
+ private BpfCoordinator.Dependencies mDeps =
+ new BpfCoordinator.Dependencies() {
+ @Override
+ int getPerformPollInterval() {
+ return DEFAULT_PERFORM_POLL_INTERVAL_MS;
+ }
+ };
+
+ @Before public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private void waitForIdle() {
+ mTestLooper.dispatchAll();
+ }
+
+ private void setupFunctioningNetdInterface() throws Exception {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
+ }
+
+ @NonNull
+ private BpfCoordinator makeBpfCoordinator() throws Exception {
+ BpfCoordinator coordinator = new BpfCoordinator(
+ new Handler(mTestLooper.getLooper()), mNetd, mStatsManager, new SharedLog("test"),
+ mDeps);
+ final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
+ tetherStatsProviderCaptor =
+ ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
+ verify(mStatsManager).registerNetworkStatsProvider(anyString(),
+ tetherStatsProviderCaptor.capture());
+ mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
+ assertNotNull(mTetherStatsProvider);
+ mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
+ mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
+ return coordinator;
+ }
+
+ @NonNull
+ private static NetworkStats.Entry buildTestEntry(@NonNull StatsType how,
+ @NonNull String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ return new NetworkStats.Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING,
+ SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes,
+ rxPackets, txBytes, txPackets, 0L);
+ }
+
+ @NonNull
+ private static TetherStatsParcel buildTestTetherStatsParcel(@NonNull Integer ifIndex,
+ long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ final TetherStatsParcel parcel = new TetherStatsParcel();
+ parcel.ifIndex = ifIndex;
+ parcel.rxBytes = rxBytes;
+ parcel.rxPackets = rxPackets;
+ parcel.txBytes = txBytes;
+ parcel.txPackets = txPackets;
+ return parcel;
+ }
+
+ private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS);
+ waitForIdle();
+ }
+
+ @Test
+ public void testGetForwardedStats() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.start();
+
+ final String wlanIface = "wlan0";
+ final Integer wlanIfIndex = 100;
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 101;
+
+ // Add interface name to lookup table. In realistic case, the upstream interface name will
+ // be added by IpServer when IpServer has received with a new IPv6 upstream update event.
+ coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // [1] Both interface stats are changed.
+ // Setup the tether stats of wlan and mobile interface. Note that move forward the time of
+ // the looper to make sure the new tether stats has been updated by polling update thread.
+ setTetherOffloadStatsList(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
+ buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
+
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 1000, 100, 2000, 200))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 3000, 300, 4000, 400));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 1000, 100, 2000, 200))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 3000, 300, 4000, 400));
+
+ // Force pushing stats update to verify the stats reported.
+ // TODO: Perhaps make #expectNotifyStatsUpdated to use test TetherStatsParcel object for
+ // verifying the notification.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+
+ // [2] Only one interface stats is changed.
+ // The tether stats of mobile interface is accumulated and The tether stats of wlan
+ // interface is the same.
+ setTetherOffloadStatsList(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
+ buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
+
+ final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 0, 0, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 10, 20, 30, 40));
+
+ final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 0, 0, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 10, 20, 30, 40));
+
+ // Force pushing stats update to verify that only diff of stats is reported.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
+ expectedUidStatsDiff);
+
+ // [3] Stop coordinator.
+ // Shutdown the coordinator and clear the invocation history, especially the
+ // tetherOffloadGetStats() calls.
+ coordinator.stop();
+ clearInvocations(mNetd);
+
+ // Verify the polling update thread stopped.
+ mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS);
+ waitForIdle();
+ verify(mNetd, never()).tetherOffloadGetStats();
+ }
+}