Merge "[ST02] Add methods for synthesizing DNS packets"
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 67e9c93..2534d61 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -15,13 +15,17 @@
  */
 package com.android.net.module.util;
 
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.Base64;
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
+import java.io.PrintWriter;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.function.BiFunction;
 
 /**
  * The classes and the methods for BPF dump utilization.
@@ -74,6 +78,38 @@
         return new Pair<>(k, v);
     }
 
+    /**
+     * Dump the BpfMap name and entries
+     */
+    public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
+            PrintWriter pw, String mapName, BiFunction<K, V, String> entryToString) {
+        dumpMap(map, pw, mapName, "" /* header */, entryToString);
+    }
+
+    /**
+     * Dump the BpfMap name, header, and entries
+     */
+    public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
+            PrintWriter pw, String mapName, String header, BiFunction<K, V, String> entryToString) {
+        pw.println(mapName + ":");
+        if (!header.isEmpty()) {
+            pw.println("  " + header);
+        }
+        try {
+            map.forEach((key, value) -> {
+                // Value could be null if there is a concurrent entry deletion.
+                // http://b/220084230.
+                if (value != null) {
+                    pw.println("  " + entryToString.apply(key, value));
+                } else {
+                    pw.println("Entry is deleted while dumping, iterating from first entry");
+                }
+            });
+        } catch (ErrnoException e) {
+            pw.println("Map dump end with error: " + Os.strerror(e.errno));
+        }
+    }
+
     // TODO: add a helper to dump bpf map content with the map name, the header line
     // (ex: "BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif"), a lambda that
     // knows how to dump each line, and the PrintWriter.
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index f7019a5..2267f31 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -24,9 +24,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.net.module.util.Struct;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -104,22 +101,6 @@
         mValueSize = Struct.getSize(value);
     }
 
-     /**
-     * Constructor for testing only.
-     * The derived class implements an internal mocked map. It need to implement all functions
-     * which are related with the native BPF map because the BPF map handler is not initialized.
-     * See BpfCoordinatorTest#TestBpfMap.
-     * TODO: remove once TestBpfMap derive from IBpfMap.
-     */
-    @VisibleForTesting
-    protected BpfMap(final Class<K> key, final Class<V> value) {
-        mMapFd = null;  // unused
-        mKeyClass = key;
-        mValueClass = value;
-        mKeySize = Struct.getSize(key);
-        mValueSize = Struct.getSize(value);
-    }
-
     /**
      * Update an existing or create a new key -> value entry in an eBbpf map.
      * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 30a1c33..f8f250d 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -259,4 +259,17 @@
             return defaultValue;
         }
     }
+
+    /**
+     * Gets int config from resources.
+     */
+    public static int getResIntegerConfig(@NonNull final Context context,
+            @BoolRes int configResource, final int defaultValue) {
+        final Resources res = context.getResources();
+        try {
+            return res.getInteger(configResource);
+        } catch (Resources.NotFoundException e) {
+            return defaultValue;
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
index dce369a..83ff875 100644
--- a/staticlibs/device/com/android/net/module/util/IBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -19,6 +19,7 @@
 
 import androidx.annotation.NonNull;
 
+import java.io.IOException;
 import java.util.NoSuchElementException;
 
 /**
@@ -74,4 +75,8 @@
 
     /** Clears the map. */
     void clear() throws ErrnoException;
+
+    /** Close for AutoCloseable. */
+    @Override
+    void close() throws IOException;
 }
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index d1728c2..7cac90d 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -99,7 +99,8 @@
      * @return The index of the first element that matches the predicate, or -1 if none.
      */
     @Nullable
-    public static <T> int indexOf(@NonNull Collection<T> elem, @NonNull Predicate<T> predicate) {
+    public static <T> int indexOf(@NonNull final Collection<T> elem,
+            @NonNull final Predicate<? super T> predicate) {
         int idx = 0;
         for (final T e : elem) {
             if (predicate.test(e)) return idx;
@@ -222,7 +223,8 @@
      * @param <T> type of elements
      * @return true if |haystack| contains any of the |needles|, false otherwise
      */
-    public static <T> boolean containsAny(Collection<T> haystack, Collection<? extends T> needles) {
+    public static <T> boolean containsAny(@NonNull final Collection<T> haystack,
+            @NonNull final Collection<? extends T> needles) {
         for (T needle : needles) {
             if (haystack.contains(needle)) return true;
         }
@@ -236,7 +238,8 @@
      * @param <T> type of elements
      * @return true if |haystack| contains all of the |needles|, false otherwise
      */
-    public static <T> boolean containsAll(Collection<T> haystack, Collection<? extends T> needles) {
+    public static <T> boolean containsAll(@NonNull final Collection<T> haystack,
+            @NonNull final Collection<? extends T> needles) {
         return haystack.containsAll(needles);
     }
 
@@ -248,7 +251,8 @@
      * @return The first element matching the predicate, or null if none.
      */
     @Nullable
-    public static <T> T findFirst(Collection<T> haystack, Predicate<? super T> condition) {
+    public static <T> T findFirst(@NonNull final Collection<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
         for (T needle : haystack) {
             if (condition.test(needle)) return needle;
         }
@@ -267,11 +271,24 @@
     // wasteful (store and reverse a copy, test all elements, or recurse to the end of the
     // list to test on the up path and possibly blow the call stack)
     @Nullable
-    public static <T> T findLast(List<T> haystack, Predicate<? super T> condition) {
+    public static <T> T findLast(@NonNull final List<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
         for (int i = haystack.size() - 1; i >= 0; --i) {
             final T needle = haystack.get(i);
             if (condition.test(needle)) return needle;
         }
         return null;
     }
+
+    /**
+     * Returns whether a collection contains an element matching a condition
+     * @param haystack The collection to search.
+     * @param condition The predicate to match.
+     * @param <T> The type of element in the collection.
+     * @return Whether the collection contains any element matching the condition.
+     */
+    public static <T> boolean contains(@NonNull final Collection<T> haystack,
+            @NonNull final Predicate<? super T> condition) {
+        return -1 != indexOf(haystack, condition);
+    }
 }
diff --git a/staticlibs/netd/libnetdutils/Syscalls.cpp b/staticlibs/netd/libnetdutils/Syscalls.cpp
index 9f653f7..7e1a242 100644
--- a/staticlibs/netd/libnetdutils/Syscalls.cpp
+++ b/staticlibs/netd/libnetdutils/Syscalls.cpp
@@ -26,7 +26,7 @@
 
 // Retry syscall fn as long as it returns -1 with errno == EINTR
 template <typename FnT, typename... Params>
-typename std::result_of<FnT(Params...)>::type syscallRetry(FnT fn, Params&&... params) {
+typename std::invoke_result<FnT, Params...>::type syscallRetry(FnT fn, Params&&... params) {
     auto rv = fn(std::forward<Params>(params)...);
     while ((rv == -1) && (errno == EINTR)) {
         rv = fn(std::forward<Params>(params)...);
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
index 395011c..3932925 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -18,15 +18,21 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.TestBpfMap;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class BpfDumpTest {
@@ -79,4 +85,37 @@
         assertThrowsIllegalArgumentException(
                 TEST_KEY_VAL_BASE64 + BASE64_DELIMITER + TEST_KEY_BASE64);
     }
+
+    private String getDumpMap(final IBpfMap<Struct.U32, Struct.U32> map) {
+        final StringWriter sw = new StringWriter();
+        BpfDump.dumpMap(map, new PrintWriter(sw), "mapName", "header",
+                (key, val) -> "key=" + key.val + ", val=" + val.val);
+        return sw.toString();
+    }
+
+    @Test
+    public void testDumpMap() throws Exception {
+        final IBpfMap<Struct.U32, Struct.U32> map =
+                new TestBpfMap<>(Struct.U32.class, Struct.U32.class);
+        map.updateEntry(new Struct.U32(123), new Struct.U32(456));
+
+        final String dump = getDumpMap(map);
+        assertEquals(dump, "mapName:\n"
+                + "  header\n"
+                + "  key=123, val=456\n");
+    }
+
+    @Test
+    public void testDumpMapMultipleEntries() throws Exception {
+        final IBpfMap<Struct.U32, Struct.U32> map =
+                new TestBpfMap<>(Struct.U32.class, Struct.U32.class);
+        map.updateEntry(new Struct.U32(123), new Struct.U32(456));
+        map.updateEntry(new Struct.U32(789), new Struct.U32(123));
+
+        final String dump = getDumpMap(map);
+        assertTrue(dump.contains("mapName:"));
+        assertTrue(dump.contains("header"));
+        assertTrue(dump.contains("key=123, val=456"));
+        assertTrue(dump.contains("key=789, val=123"));
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 0991352..0f00d0b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -94,6 +94,12 @@
         assertTrue(CollectionUtils.contains(arrayOf("A", "B", "C"), "C"))
         assertFalse(CollectionUtils.contains(arrayOf("A", "B", "C"), "D"))
         assertFalse(CollectionUtils.contains(null, "A"))
+
+        val list = listOf("A", "B", "Ab", "C", "D", "E", "A", "E")
+        assertTrue(CollectionUtils.contains(list) { it.length == 2 })
+        assertFalse(CollectionUtils.contains(list) { it.length < 1 })
+        assertTrue(CollectionUtils.contains(list) { it > "A" })
+        assertFalse(CollectionUtils.contains(list) { it > "F" })
     }
 
     @Test
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index c65f793..a8e7993 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -283,4 +283,13 @@
         doThrow(new Resources.NotFoundException()).when(mResources).getBoolean(someResId);
         assertFalse(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
     }
+
+    @Test
+    public void testGetResIntegerConfig() {
+        final int someResId = 1234;
+        doReturn(2097).when(mResources).getInteger(someResId);
+        assertEquals(2097, DeviceConfigUtils.getResIntegerConfig(mContext, someResId, 2098));
+        doThrow(new Resources.NotFoundException()).when(mResources).getInteger(someResId);
+        assertEquals(2098, DeviceConfigUtils.getResIntegerConfig(mContext, someResId, 2098));
+    }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
index 98464eb..cbdc017 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
@@ -2,7 +2,6 @@
 
 import android.os.SystemClock
 import java.util.concurrent.CyclicBarrier
-import kotlin.system.measureTimeMillis
 import kotlin.test.assertEquals
 import kotlin.test.assertFails
 import kotlin.test.assertNull
@@ -17,14 +16,18 @@
 val INTERPRET_TIME_UNIT = 40L // ms
 
 /**
- * A small interpreter for testing parallel code. The interpreter will read a list of lines
- * consisting of "|"-separated statements. Each column runs in a different concurrent thread
- * and all threads wait for each other in between lines. Each statement is split on ";" then
- * matched with regular expressions in the instructionTable constant, which contains the
- * code associated with each statement. The interpreter supports an object being passed to
- * the interpretTestSpec() method to be passed in each lambda (think about the object under
- * test), and an optional transform function to be executed on the object at the start of
- * every thread.
+ * A small interpreter for testing parallel code.
+ *
+ * The interpreter will read a list of lines consisting of "|"-separated statements, e.g. :
+ *   sleep 2 ; unblock thread2 | wait thread2 time 2..5
+ *   sendMessage "x"           | obtainMessage = "x" time 0..1
+ *
+ * Each column runs in a different concurrent thread and all threads wait for each other in
+ * between lines. Each statement is split on ";" then matched with regular expressions in the
+ * instructionTable constant, which contains the code associated with each statement. The
+ * interpreter supports an object being passed to the interpretTestSpec() method to be passed
+ * in each lambda (think about the object under test), and an optional transform function to be
+ * executed on the object at the start of every thread.
  *
  * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
  * value but can be passed to the constructor. Whitespace is ignored.
@@ -34,13 +37,8 @@
  * the regexp match, and the object. See the individual tests for the DSL of that test.
  * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
  * constant below for an example of how to write an interpreting table.
- * Some expressions already exist by default and can be used by all interpreters. They include :
- * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
- * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
- *   string "null" or an int. Returns Unit.
- * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
- *   y time units.
- * EXPR // any text : comments are ignored.
+ * Some expressions already exist by default and can be used by all interpreters. Refer to
+ * getDefaultInstructions() below for a list and documentation.
  */
 open class ConcurrentInterpreter<T>(
     localInterpretTable: List<InterpretMatcher<T>>,
@@ -48,6 +46,9 @@
 ) {
     private val interpretTable: List<InterpretMatcher<T>> =
             localInterpretTable + getDefaultInstructions()
+    // The last time the thread became blocked, with base System.currentTimeMillis(). This should
+    // be set immediately before any time the thread gets blocked.
+    internal val lastBlockedTime = ThreadLocal<Long>()
 
     // Split the line into multiple statements separated by ";" and execute them. Return whatever
     // the last statement returned.
@@ -63,11 +64,28 @@
         return code(this, r, match)
     }
 
-    // Spins as many threads as needed by the test spec and interpret each program concurrently,
-    // having all threads waiting on a CyclicBarrier after each line.
-    // |lineShift| says how many lines after the call the spec starts. This is used for error
-    // reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
-    // than the line at which the expression starts.
+    /**
+     * Spins as many threads as needed by the test spec and interpret each program concurrently.
+     *
+     * All threads wait on a CyclicBarrier after each line.
+     * |lineShift| says how many lines after the call the spec starts. This is used for error
+     * reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
+     * than the line at which the expression starts.
+     *
+     * This method is mostly meant for implementations that extend the ConcurrentInterpreter
+     * class to add their own directives and instructions. These may need to operate on some
+     * data, which can be passed in |initial|. For example, an interpreter specialized in callbacks
+     * may want to pass the callback there. In some cases, it's necessary that each thread
+     * performs a transformation *after* it starts on that value before starting ; in this case,
+     * the transformation can be passed to |threadTransform|. The default is to return |initial| as
+     * is. Look at some existing child classes of this interpreter for some examples of how this
+     * can be used.
+     *
+     * @param spec The test spec, as a string of lines separated by pipes.
+     * @param initial An initial value passed to all threads.
+     * @param lineShift How many lines after the call the spec starts, for error reporting.
+     * @param threadTransform an optional transformation that each thread will apply to |initial|
+     */
     fun interpretTestSpec(
         spec: String,
         initial: T,
@@ -77,16 +95,25 @@
         // For nice stack traces
         val callSite = getCallingMethod()
         val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
-        // |threads| contains arrays of strings that make up the statements of a thread : in other
+        // |lines| contains arrays of strings that make up the statements of a thread : in other
         // words, it's an array that contains a list of statements for each column in the spec.
+        // E.g. if the string is """
+        //   a | b | c
+        //   d | e | f
+        // """, then lines is [ [ "a", "b", "c" ], [ "d", "e", "f" ] ].
         val threadCount = lines[0].size
         assertTrue(lines.all { it.size == threadCount })
         val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
+        // |threadInstructions| is a list where each element is the list of instructions for the
+        // thread at the index. In other words, it's just |lines| transposed. In the example
+        // above, it would be [ [ "a", "d" ], [ "b", "e" ], [ "c", "f" ] ]
+        // mapIndexed below will pass in |instructions| the list of instructions for this thread.
         val barrier = CyclicBarrier(threadCount)
         var crash: InterpretException? = null
         threadInstructions.mapIndexed { threadIndex, instructions ->
             Thread {
                 val threadLocal = threadTransform(initial)
+                lastBlockedTime.set(System.currentTimeMillis())
                 barrier.await()
                 var lineNum = 0
                 instructions.forEach {
@@ -104,6 +131,7 @@
                                 callSite.lineNumber + lineNum + lineShift,
                                 callSite.className, callSite.methodName, callSite.fileName, e)
                     }
+                    lastBlockedTime.set(System.currentTimeMillis())
                     barrier.await()
                 }
             }.also { it.start() }
@@ -138,6 +166,16 @@
     }
 }
 
+/**
+ * Default instructions available to all interpreters.
+ * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
+ * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
+ *   string "null" or an int. Returns Unit.
+ * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
+ *   y time units.
+ * EXPR // any text : comments are ignored.
+ * EXPR fails : checks that EXPR throws some exception.
+ */
 private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
     // Interpret an empty line as doing nothing.
     Regex("") to { _, _, _ -> null },
@@ -145,8 +183,25 @@
     Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
     // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
     Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
-        val time = measureTimeMillis { i.interpret(r.strArg(1), t) }
-        assertTrue(time in r.timeArg(2)..r.timeArg(3), "$time not in ${r.timeArg(2)..r.timeArg(3)}")
+        val lateStart = System.currentTimeMillis()
+        i.interpret(r.strArg(1), t)
+        val end = System.currentTimeMillis()
+        // There is uncertainty in measuring time.
+        // It takes some (small) time for the thread to even measure the time at which it
+        // starts interpreting the instruction. It is therefore possible that thread A sleeps for
+        // n milliseconds, and B expects to have waited for at least n milliseconds, but because
+        // B started measuring after 1ms or so, B thinks it didn't wait long enough.
+        // To avoid this, when the `time` instruction tests the instruction took at least X and
+        // at most Y, it tests X against a time measured since *before* the thread blocked but
+        // Y against a time measured as late as possible. This ensures that the timer is
+        // sufficiently lenient in both directions that there are no flaky measures.
+        val minTime = end - lateStart
+        val maxTime = end - i.lastBlockedTime.get()!!
+
+        assertTrue(maxTime >= r.timeArg(2),
+                "Should have taken at least ${r.timeArg(2)} but took less than $maxTime")
+        assertTrue(minTime <= r.timeArg(3),
+                "Should have taken at most ${r.timeArg(3)} but took more than $minTime")
     },
     // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
     Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 3883511..733bd98 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -20,10 +20,10 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.Struct;
 
+import java.io.IOException;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
@@ -32,20 +32,19 @@
 
 /**
  *
- * Fake BPF map class for tests that have no no privilege to access real BPF maps. All member
- * functions which eventually call JNI to access the real native BPF map are overridden.
+ * Fake BPF map class for tests that have no privilege to access real BPF maps. TestBpfMap does not
+ * load JNI and all member functions do not access real BPF maps.
  *
- * Inherits from BpfMap instead of implementing IBpfMap so that any class using a BpfMap can use
- * this class in its tests.
+ * Implements IBpfMap so that any class using IBpfMap can use this class in its tests.
  *
  * @param <K> the key type
  * @param <V> the value type
  */
-public class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+public class TestBpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
     private final ConcurrentHashMap<K, V> mMap = new ConcurrentHashMap<>();
 
+    // TODO: Remove this constructor
     public TestBpfMap(final Class<K> key, final Class<V> value) {
-        super(key, value);
     }
 
     @Override
@@ -133,4 +132,8 @@
         // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
         mMap.clear();
     }
+
+    @Override
+    public void close() throws IOException {
+    }
 }