Merge "Fix LocationPermissionCheckerTest"
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index 73c2431..ee43ae9 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -21,6 +21,7 @@
 import static com.android.net.module.util.IpUtils.icmpv6Checksum;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
 
@@ -30,6 +31,7 @@
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.NaHeader;
+import com.android.net.module.util.structs.NsHeader;
 import com.android.net.module.util.structs.RaHeader;
 import com.android.net.module.util.structs.RsHeader;
 
@@ -123,6 +125,19 @@
     }
 
     /**
+     * Build an ICMPv6 Neighbor Solicitation packet from the required specified parameters.
+     */
+    public static ByteBuffer buildNsPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp,
+            final Inet6Address target, final ByteBuffer... options) {
+        final NsHeader nsHeader = new NsHeader(target);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(nsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_NEIGHBOR_SOLICITATION /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
      * Build an ICMPv6 Router Solicitation packet from the required specified parameters.
      */
     public static ByteBuffer buildRsPacket(final MacAddress srcMac, final MacAddress dstMac,
diff --git a/staticlibs/device/com/android/net/module/util/structs/NaHeader.java b/staticlibs/device/com/android/net/module/util/structs/NaHeader.java
index 571d67b..90c078e 100644
--- a/staticlibs/device/com/android/net/module/util/structs/NaHeader.java
+++ b/staticlibs/device/com/android/net/module/util/structs/NaHeader.java
@@ -26,8 +26,8 @@
  * ICMPv6 Neighbor Advertisement header, follow {@link Icmpv6Header}, as per
  * https://tools.ietf.org/html/rfc4861. This does not contain any option.
  *
- * 0                   1                   2                   3
- * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  0                   1                   2                   3
+ *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |     Type      |     Code      |          Checksum             |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
diff --git a/staticlibs/device/com/android/net/module/util/structs/NsHeader.java b/staticlibs/device/com/android/net/module/util/structs/NsHeader.java
new file mode 100644
index 0000000..6e0aa50
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/NsHeader.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/**
+ * ICMPv6 Neighbor Solicitation header, follow {@link Icmpv6Header}, as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ *  0                   1                   2                   3
+ *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |     Code      |          Checksum             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Reserved                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                       Target Address                          +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class NsHeader extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public int reserved; // 32 Reserved bits.
+    @Field(order = 1, type = Type.Ipv6Address)
+    public Inet6Address target;
+
+    public NsHeader(final Inet6Address target) {
+        this.reserved = 0;
+        this.target = target;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
index 6300328..31d0729 100644
--- a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
@@ -27,6 +28,8 @@
  */
 public class InetAddressUtils {
 
+    private static final int INET6_ADDR_LENGTH = 16;
+
     /**
      * Writes an InetAddress to a parcel. The address may be null. This is likely faster than
      * calling writeSerializable.
@@ -35,6 +38,13 @@
     public static void parcelInetAddress(Parcel parcel, InetAddress address, int flags) {
         byte[] addressArray = (address != null) ? address.getAddress() : null;
         parcel.writeByteArray(addressArray);
+        if (address instanceof Inet6Address) {
+            final Inet6Address v6Address = (Inet6Address) address;
+            final boolean hasScopeId = v6Address.getScopeId() != 0;
+            parcel.writeBoolean(hasScopeId);
+            if (hasScopeId) parcel.writeInt(v6Address.getScopeId());
+        }
+
     }
 
     /**
@@ -47,7 +57,14 @@
         if (addressArray == null) {
             return null;
         }
+
         try {
+            if (addressArray.length == INET6_ADDR_LENGTH) {
+                final boolean hasScopeId = in.readBoolean();
+                final int scopeId = hasScopeId ? in.readInt() : 0;
+                return Inet6Address.getByAddress(null /* host */, addressArray, scopeId);
+            }
+
             return InetAddress.getByAddress(addressArray);
         } catch (UnknownHostException e) {
             return null;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
new file mode 100644
index 0000000..2736c53
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.net.module.util;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InetAddressUtilsTest {
+
+    private InetAddress parcelUnparcelAddress(InetAddress addr) {
+        Parcel p = Parcel.obtain();
+        InetAddressUtils.parcelInetAddress(p, addr, 0 /* flags */);
+        p.setDataPosition(0);
+        byte[] marshalled = p.marshall();
+        p.recycle();
+        p = Parcel.obtain();
+        p.unmarshall(marshalled, 0, marshalled.length);
+        p.setDataPosition(0);
+        InetAddress out = InetAddressUtils.unparcelInetAddress(p);
+        p.recycle();
+        return out;
+    }
+
+    @Test
+    public void testParcelUnparcelIpv4Address() throws Exception {
+        InetAddress ipv4 = InetAddress.getByName("192.0.2.1");
+        assertEquals(ipv4, parcelUnparcelAddress(ipv4));
+    }
+
+    @Test
+    public void testParcelUnparcelIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("2001:db8::1");
+        assertEquals(ipv6, parcelUnparcelAddress(ipv6));
+    }
+
+    @Test
+    public void testParcelUnparcelScopedIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("fe80::1%42");
+        assertEquals(42, ((Inet6Address) ipv6).getScopeId());
+        Inet6Address out = (Inet6Address) parcelUnparcelAddress(ipv6);
+        assertEquals(ipv6, out);
+        assertEquals(42, out.getScopeId());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
new file mode 100644
index 0000000..37278b3
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net.module.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Ipv6UtilsTest {
+
+    private static final MacAddress MAC1 = MacAddress.fromString("11:22:33:44:55:66");
+    private static final MacAddress MAC2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+    private static final Inet6Address LINK_LOCAL = addr("fe80::1");
+    private static final Inet6Address ROUTER_LINK_LOCAL = addr("fe80::cafe:d00d");
+    private static final Inet6Address ALL_ROUTERS =
+            NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+    private static final Inet6Address ALL_NODES =
+            NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+
+    @Test
+    public void testBuildRsPacket() {
+        ByteBuffer b = Ipv6Utils.buildRsPacket(MAC1, MAC2, LINK_LOCAL, ALL_ROUTERS /* no opts */);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC1, eth.srcMac);
+        assertEquals(MAC2, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_ROUTERS, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    @Test
+    public void testBuildRaPacket() {
+        final byte pioFlags =
+                NetworkStackConstants.PIO_FLAG_AUTONOMOUS | NetworkStackConstants.PIO_FLAG_ON_LINK;
+        ByteBuffer pio1 = PrefixInformationOption.build(new IpPrefix("2001:db8:1::/64"),
+                pioFlags, 3600 /* validLifetime */, 1800 /* preferredLifetime */);
+        ByteBuffer pio2 = PrefixInformationOption.build(new IpPrefix("fdcd:a17f:6502:1::/64"),
+                pioFlags, 86400 /* validLifetime */, 86400 /* preferredLifetime */);
+
+        ByteBuffer b = Ipv6Utils.buildRaPacket(MAC2, MAC1, ROUTER_LINK_LOCAL, ALL_NODES,
+                (byte) 0 /* flags */, 7200 /* lifetime */,
+                30_000 /* reachableTime */, 750 /* retransTimer */,
+                pio1, pio2);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC2, eth.srcMac);
+        assertEquals(MAC1, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(ROUTER_LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+
+        RaHeader ra = Struct.parse(RaHeader.class, b);
+        assertEquals(0, ra.hopLimit);  // Hop limit: unspecified.
+        assertEquals(0, ra.flags);
+        assertEquals(7200, ra.lifetime);
+        assertEquals(30_000, ra.reachableTime);
+        assertEquals(750, ra.retransTimer);
+
+        PrefixInformationOption pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "2001:db8:1::/64", pioFlags, 3600, 1800);
+        pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "fdcd:a17f:6502:1::/64", pioFlags, 86400, 86400);
+    }
+
+    private void assertPioEquals(PrefixInformationOption pio, String prefix, byte flags,
+            long valid, long preferred) {
+        assertEquals(NetworkStackConstants.ICMPV6_ND_OPTION_PIO, pio.type);
+        assertEquals(4, pio.length);
+        assertEquals(flags, pio.flags);
+        assertEquals(valid, pio.validLifetime);
+        assertEquals(preferred, pio.preferredLifetime);
+        IpPrefix expected = new IpPrefix(prefix);
+        IpPrefix actual = new IpPrefix(pio.prefix, pio.prefixLen);
+        assertEquals(expected, actual);
+    }
+
+    private static Inet6Address addr(String addr) {
+        return (Inet6Address) InetAddresses.parseNumericAddress(addr);
+    }
+
+    private byte[] slice(byte[] array, int length) {
+        return Arrays.copyOf(array, length);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
new file mode 100644
index 0000000..9fb4d8c
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2019 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.net.module.util
+
+import com.android.testutils.ConcurrentInterpreter
+import com.android.testutils.InterpretException
+import com.android.testutils.InterpretMatcher
+import com.android.testutils.SyntaxException
+import com.android.testutils.__FILE__
+import com.android.testutils.__LINE__
+import com.android.testutils.intArg
+import com.android.testutils.strArg
+import com.android.testutils.timeArg
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.util.concurrent.CyclicBarrier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.system.measureTimeMillis
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+val TEST_VALUES = listOf(4, 13, 52, 94, 41, 68, 11, 13, 51, 0, 91, 94, 33, 98, 14)
+const val ABSENT_VALUE = 2
+// Caution in changing these : some tests rely on the fact that TEST_TIMEOUT > 2 * SHORT_TIMEOUT
+// and LONG_TIMEOUT > 2 * TEST_TIMEOUT
+const val SHORT_TIMEOUT = 40L // ms
+const val TEST_TIMEOUT = 200L // ms
+const val LONG_TIMEOUT = 5000L // ms
+
+@RunWith(JUnit4::class)
+class TrackRecordTest {
+    @Test
+    fun testAddAndSizeAndGet() {
+        val repeats = 22 // arbitrary
+        val record = ArrayTrackRecord<Int>()
+        assertEquals(0, record.size)
+        repeat(repeats) { i -> record.add(i + 2) }
+        assertEquals(repeats, record.size)
+        record.add(2)
+        assertEquals(repeats + 1, record.size)
+
+        assertEquals(11, record[9])
+        assertEquals(11, record.getOrNull(9))
+        assertEquals(2, record[record.size - 1])
+        assertEquals(2, record.getOrNull(record.size - 1))
+
+        assertFailsWith<IndexOutOfBoundsException> { record[800] }
+        assertFailsWith<IndexOutOfBoundsException> { record[-1] }
+        assertFailsWith<IndexOutOfBoundsException> { record[repeats + 1] }
+        assertNull(record.getOrNull(800))
+        assertNull(record.getOrNull(-1))
+        assertNull(record.getOrNull(repeats + 1))
+        assertNull(record.getOrNull(800) { true })
+        assertNull(record.getOrNull(-1) { true })
+        assertNull(record.getOrNull(repeats + 1) { true })
+    }
+
+    @Test
+    fun testIndexOf() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        with(record) {
+            assertEquals(9, indexOf(0))
+            assertEquals(9, lastIndexOf(0))
+            assertEquals(1, indexOf(13))
+            assertEquals(7, lastIndexOf(13))
+            assertEquals(3, indexOf(94))
+            assertEquals(11, lastIndexOf(94))
+            assertEquals(-1, indexOf(ABSENT_VALUE))
+            assertEquals(-1, lastIndexOf(ABSENT_VALUE))
+        }
+    }
+
+    @Test
+    fun testContains() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        TEST_VALUES.forEach { assertTrue(record.contains(it)) }
+        assertFalse(record.contains(ABSENT_VALUE))
+        assertTrue(record.containsAll(TEST_VALUES))
+        assertTrue(record.containsAll(TEST_VALUES.sorted()))
+        assertTrue(record.containsAll(TEST_VALUES.sortedDescending()))
+        assertTrue(record.containsAll(TEST_VALUES.distinct()))
+        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2)))
+        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2).sorted()))
+        assertTrue(record.containsAll(listOf()))
+        assertFalse(record.containsAll(listOf(ABSENT_VALUE)))
+        assertFalse(record.containsAll(TEST_VALUES + listOf(ABSENT_VALUE)))
+    }
+
+    @Test
+    fun testEmpty() {
+        val record = ArrayTrackRecord<Int>()
+        assertTrue(record.isEmpty())
+        record.add(1)
+        assertFalse(record.isEmpty())
+    }
+
+    @Test
+    fun testIterate() {
+        val record = ArrayTrackRecord<Int>()
+        record.forEach { fail("Expected nothing to iterate") }
+        TEST_VALUES.forEach { record.add(it) }
+        // zip relies on the iterator (this calls extension function Iterable#zip(Iterable))
+        record.zip(TEST_VALUES).forEach { assertEquals(it.first, it.second) }
+        // Also test reverse iteration (to test hasPrevious() and friends)
+        record.reversed().zip(TEST_VALUES.reversed()).forEach { assertEquals(it.first, it.second) }
+    }
+
+    @Test
+    fun testIteratorIsSnapshot() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        val iterator = record.iterator()
+        val expectedSize = record.size
+        record.add(ABSENT_VALUE)
+        record.add(ABSENT_VALUE)
+        var measuredSize = 0
+        iterator.forEach {
+            ++measuredSize
+            assertNotEquals(ABSENT_VALUE, it)
+        }
+        assertEquals(expectedSize, measuredSize)
+    }
+
+    @Test
+    fun testSublist() {
+        val record = ArrayTrackRecord<Int>()
+        TEST_VALUES.forEach { record.add(it) }
+        assertEquals(record.subList(3, record.size - 3),
+                TEST_VALUES.subList(3, TEST_VALUES.size - 3))
+    }
+
+    fun testPollReturnsImmediately(record: TrackRecord<Int>) {
+        record.add(4)
+        val elapsed = measureTimeMillis { assertEquals(4, record.poll(LONG_TIMEOUT, 0)) }
+        // Should not have waited at all, in fact.
+        assertTrue(elapsed < LONG_TIMEOUT)
+        record.add(7)
+        record.add(9)
+        // Can poll multiple times for the same position, in whatever order
+        assertEquals(9, record.poll(0, 2))
+        assertEquals(7, record.poll(Long.MAX_VALUE, 1))
+        assertEquals(9, record.poll(0, 2))
+        assertEquals(4, record.poll(0, 0))
+        assertEquals(9, record.poll(0, 2) { it > 5 })
+        assertEquals(7, record.poll(0, 0) { it > 5 })
+    }
+
+    @Test
+    fun testPollReturnsImmediately() {
+        testPollReturnsImmediately(ArrayTrackRecord())
+        testPollReturnsImmediately(ArrayTrackRecord<Int>().newReadHead())
+    }
+
+    @Test
+    fun testPollTimesOut() {
+        val record = ArrayTrackRecord<Int>()
+        var delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0)) }
+        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
+        assertTrue(delay >= SHORT_TIMEOUT)
+    }
+
+    @Test
+    fun testConcurrentPollDisallowed() {
+        val failures = AtomicInteger(0)
+        val readHead = ArrayTrackRecord<Int>().newReadHead()
+        val barrier = CyclicBarrier(2)
+        Thread {
+            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
+            try {
+                readHead.poll(LONG_TIMEOUT)
+            } catch (e: ConcurrentModificationException) {
+                failures.incrementAndGet()
+                // Unblock the other thread
+                readHead.add(0)
+            }
+        }.start()
+        barrier.await() // barrier 1
+        try {
+            readHead.poll(LONG_TIMEOUT)
+        } catch (e: ConcurrentModificationException) {
+            failures.incrementAndGet()
+            // Unblock the other thread
+            readHead.add(0)
+        }
+        // One of the threads must have gotten an exception.
+        assertEquals(failures.get(), 1)
+    }
+
+    @Test
+    fun testPollWakesUp() {
+        val record = ArrayTrackRecord<Int>()
+        val barrier = CyclicBarrier(2)
+        Thread {
+            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
+            barrier.await() // barrier 2
+            Thread.sleep(SHORT_TIMEOUT * 2)
+            record.add(31)
+        }.start()
+        barrier.await() // barrier 1
+        // Should find the element in more than SHORT_TIMEOUT but less than TEST_TIMEOUT
+        var delay = measureTimeMillis {
+            barrier.await() // barrier 2
+            assertEquals(31, record.poll(TEST_TIMEOUT, 0))
+        }
+        assertTrue(delay in SHORT_TIMEOUT..TEST_TIMEOUT)
+        // Polling for an element already added in anothe thread (pos 0) : should return immediately
+        delay = measureTimeMillis { assertEquals(31, record.poll(TEST_TIMEOUT, 0)) }
+        assertTrue(delay < TEST_TIMEOUT, "Delay $delay > $TEST_TIMEOUT")
+        // Waiting for an element that never comes
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 1)) }
+        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
+        // Polling for an element that doesn't match what is already there
+        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
+        assertTrue(delay >= SHORT_TIMEOUT)
+    }
+
+    // Just make sure the interpreter actually throws an exception when the spec
+    // does not conform to the behavior. The interpreter is just a tool to test a
+    // tool used for a tool for test, let's not have hundreds of tests for it ;
+    // if it's broken one of the tests using it will break.
+    @Test
+    fun testInterpreter() {
+        val interpretLine = __LINE__ + 2
+        try {
+            TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+                add(4) | poll(1, 0) = 5
+            """)
+            fail("This spec should have thrown")
+        } catch (e: InterpretException) {
+            assertTrue(e.cause is AssertionError)
+            assertEquals(interpretLine + 1, e.stackTrace[0].lineNumber)
+            assertTrue(e.stackTrace[0].fileName.contains(__FILE__))
+            assertTrue(e.stackTrace[0].methodName.contains("testInterpreter"))
+            assertTrue(e.stackTrace[0].methodName.contains("thread1"))
+        }
+    }
+
+    @Test
+    fun testMultipleAdds() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(2)         |                |                |
+                           | add(4)         |                |
+                           |                | add(6)         |
+                           |                |                | add(8)
+            poll(0, 0) = 2 time 0..1 | poll(0, 0) = 2 | poll(0, 0) = 2 | poll(0, 0) = 2
+            poll(0, 1) = 4 time 0..1 | poll(0, 1) = 4 | poll(0, 1) = 4 | poll(0, 1) = 4
+            poll(0, 2) = 6 time 0..1 | poll(0, 2) = 6 | poll(0, 2) = 6 | poll(0, 2) = 6
+            poll(0, 3) = 8 time 0..1 | poll(0, 3) = 8 | poll(0, 3) = 8 | poll(0, 3) = 8
+        """)
+    }
+
+    @Test
+    fun testConcurrentAdds() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(2)             | add(4)             | add(6)             | add(8)
+            add(1)             | add(3)             | add(5)             | add(7)
+            poll(0, 1) is even | poll(0, 0) is even | poll(0, 3) is even | poll(0, 2) is even
+            poll(0, 5) is odd  | poll(0, 4) is odd  | poll(0, 7) is odd  | poll(0, 6) is odd
+        """)
+    }
+
+    @Test
+    fun testMultiplePoll() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+            add(4)         | poll(1, 0) = 4
+                           | poll(0, 1) = null time 0..1
+                           | poll(1, 1) = null time 1..2
+            sleep; add(7)  | poll(2, 1) = 7 time 1..2
+            sleep; add(18) | poll(2, 2) = 18 time 1..2
+        """)
+    }
+
+    @Test
+    fun testMultiplePollWithPredicate() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
+                     | poll(1, 0) = null          | poll(1, 0) = null
+            add(6)   | poll(1, 0) = 6             |
+            add(11)  | poll(1, 0) { > 20 } = null | poll(1, 0) { = 11 } = 11
+                     | poll(1, 0) { > 8 } = 11    |
+        """)
+    }
+
+    @Test
+    fun testMultipleReadHeads() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+                   | poll() = null | poll() = null | poll() = null
+            add(5) |               | poll() = 5    |
+                   | poll() = 5    |               |
+            add(8) | poll() = 8    | poll() = 8    |
+                   |               |               | poll() = 5
+                   |               |               | poll() = 8
+                   |               |               | poll() = null
+                   |               | poll() = null |
+        """)
+    }
+
+    @Test
+    fun testReadHeadPollWithPredicate() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(5)  | poll() { < 0 } = null
+                    | poll() { > 5 } = null
+            add(10) |
+                    | poll() { = 5 } = null   // The "5" was skipped in the previous line
+            add(15) | poll() { > 8 } = 15     // The "10" was skipped in the previous line
+                    | poll(1, 0) { > 8 } = 10 // 10 is the first element after pos 0 matching > 8
+        """)
+    }
+
+    @Test
+    fun testPollImmediatelyAdvancesReadhead() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(1)                  | add(2)              | add(3)   | add(4)
+            mark = 0                | poll(0) { > 3 } = 4 |          |
+            poll(0) { > 10 } = null |                     |          |
+            mark = 4                |                     |          |
+            poll() = null           |                     |          |
+        """)
+    }
+
+    @Test
+    fun testParallelReadHeads() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            mark = 0   | mark = 0   | mark = 0   | mark = 0
+            add(2)     |            |            |
+                       | add(4)     |            |
+                       |            | add(6)     |
+                       |            |            | add(8)
+            poll() = 2 | poll() = 2 | poll() = 2 | poll() = 2
+            poll() = 4 | poll() = 4 | poll() = 4 | poll() = 4
+            poll() = 6 | poll() = 6 | poll() = 6 | mark = 2
+            poll() = 8 | poll() = 8 | mark = 3   | poll() = 6
+            mark = 4   | mark = 4   | poll() = 8 | poll() = 8
+        """)
+    }
+
+    @Test
+    fun testPeek() {
+        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
+            add(2)     |            |               |
+                       | add(4)     |               |
+                       |            | add(6)        |
+                       |            |               | add(8)
+            peek() = 2 | poll() = 2 | poll() = 2    | peek() = 2
+            peek() = 2 | peek() = 4 | poll() = 4    | peek() = 2
+            peek() = 2 | peek() = 4 | peek() = 6    | poll() = 2
+            peek() = 2 | mark = 1   | mark = 2      | poll() = 4
+            mark = 0   | peek() = 4 | peek() = 6    | peek() = 6
+            poll() = 2 | poll() = 4 | poll() = 6    | poll() = 6
+            poll() = 4 | mark = 2   | poll() = 8    | peek() = 8
+            peek() = 6 | peek() = 6 | peek() = null | mark = 3
+        """)
+    }
+}
+
+private object TRTInterpreter : ConcurrentInterpreter<TrackRecord<Int>>(interpretTable) {
+    fun interpretTestSpec(spec: String, useReadHeads: Boolean) = if (useReadHeads) {
+        interpretTestSpec(spec, initial = ArrayTrackRecord(),
+                threadTransform = { (it as ArrayTrackRecord).newReadHead() })
+    } else {
+        interpretTestSpec(spec, ArrayTrackRecord())
+    }
+}
+
+/*
+ * Quick ref of supported expressions :
+ * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
+ * add(x) : calls and returns TrackRecord#add.
+ * poll(time, pos) [{ predicate }] : calls and returns TrackRecord#poll(x time units, pos).
+ *   Optionally, a predicate may be specified.
+ * poll() [{ predicate }] : calls and returns ReadHead#poll(1 time unit). Optionally, a predicate
+ *   may be specified.
+ * 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.
+ * predicate must be one of "= x", "< x" or "> x".
+ */
+private val interpretTable = listOf<InterpretMatcher<TrackRecord<Int>>>(
+    // Interpret "XXX is odd" : run XXX and assert its return value is odd ("even" works too)
+    Regex("(.*)\\s+is\\s+(even|odd)") to { i, t, r ->
+        i.interpret(r.strArg(1), t).also {
+            assertEquals((it as Int) % 2, if ("even" == r.strArg(2)) 0 else 1)
+        }
+    },
+    // Interpret "add(XXX)" as TrackRecord#add(int)
+    Regex("""add\((\d+)\)""") to { i, t, r ->
+        t.add(r.intArg(1))
+    },
+    // Interpret "poll(x, y)" as TrackRecord#poll(timeout = x * INTERPRET_TIME_UNIT, pos = y)
+    // Accepts an optional {} argument for the predicate (see makePredicate for syntax)
+    Regex("""poll\((\d+),\s*(\d+)\)\s*(\{.*\})?""") to { i, t, r ->
+        t.poll(r.timeArg(1), r.intArg(2), makePredicate(r.strArg(3)))
+    },
+    // ReadHead#poll. If this throws in the cast, the code is malformed and has passed "poll()"
+    // in a test that takes a TrackRecord that is not a ReadHead. It's technically possible to get
+    // the test code to not compile instead of throw, but it's vastly more complex and this will
+    // fail 100% at runtime any test that would not have compiled.
+    Regex("""poll\((\d+)?\)\s*(\{.*\})?""") to { i, t, r ->
+        (if (r.strArg(1).isEmpty()) i.interpretTimeUnit else r.timeArg(1)).let { time ->
+            (t as ArrayTrackRecord<Int>.ReadHead).poll(time, makePredicate(r.strArg(2)))
+        }
+    },
+    // ReadHead#mark. The same remarks apply as with ReadHead#poll.
+    Regex("mark") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).mark },
+    // ReadHead#peek. The same remarks apply as with ReadHead#poll.
+    Regex("peek\\(\\)") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).peek() }
+)
+
+// Parses a { = x } or { < x } or { > x } string and returns the corresponding predicate
+// Returns an always-true predicate for empty and null arguments
+private fun makePredicate(spec: String?): (Int) -> Boolean {
+    if (spec.isNullOrEmpty()) return { true }
+    val match = Regex("""\{\s*([<>=])\s*(\d+)\s*\}""").matchEntire(spec)
+            ?: throw SyntaxException("Predicate \"${spec}\"")
+    val arg = match.intArg(2)
+    return when (match.strArg(1)) {
+        ">" -> { i -> i > arg }
+        "<" -> { i -> i < arg }
+        "=" -> { i -> i == arg }
+        else -> throw RuntimeException("How did \"${spec}\" match this regexp ?")
+    }
+}