summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-03-05 08:20:21 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-03-05 08:20:21 +0000
commit31800f017ed0cca5c124a21595b06b16eb24aa93 (patch)
treeca60cf7e0a692f5afd6070e6c63bae4338c995b1
parent50c59f2053d0bc7acbf2929d28ffa7a28ee1718e (diff)
parent5ead40a30a4041f779a622d9a7271b347028cb62 (diff)
downloadnet-busytown-mac-infra-release.tar.gz
Snap for 9680074 from 5ead40a30a4041f779a622d9a7271b347028cb62 to busytown-mac-infra-releasebusytown-mac-infra-release
Change-Id: I0e10ef242403377479206174090741902568a4c6
-rw-r--r--client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java61
-rw-r--r--common/device/com/android/net/module/util/DeviceConfigUtils.java2
-rw-r--r--common/device/com/android/net/module/util/async/OsAccess.java66
-rw-r--r--common/device/com/android/net/module/util/structs/IaPdOption.java81
-rw-r--r--common/device/com/android/net/module/util/structs/IaPrefixOption.java89
-rw-r--r--common/framework/com/android/net/module/util/NetworkStackConstants.java3
-rw-r--r--common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java4
-rw-r--r--common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt5
-rw-r--r--common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt67
-rw-r--r--common/testutils/Android.bp1
-rw-r--r--common/testutils/devicetests/com/android/testutils/HandlerUtils.kt3
-rw-r--r--common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt84
-rw-r--r--common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java568
-rw-r--r--common/testutils/devicetests/com/android/testutils/async/RateLimiter.java131
-rw-r--r--common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt22
15 files changed, 1091 insertions, 96 deletions
diff --git a/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java b/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
new file mode 100644
index 00000000..4180732f
--- /dev/null
+++ b/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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 android.net.metrics.INetdEventListener;
+
+/**
+ * Base {@link INetdEventListener} that provides no-op implementations which can
+ * be overridden.
+ */
+public class BaseNetdEventListener extends INetdEventListener.Stub {
+
+ @Override
+ public void onDnsEvent(int netId, int eventType, int returnCode,
+ int latencyMs, String hostname, String[] ipAddresses,
+ int ipAddressesCount, int uid) { }
+
+ @Override
+ public void onPrivateDnsValidationEvent(int netId, String ipAddress,
+ String hostname, boolean validated) { }
+
+ @Override
+ public void onConnectEvent(int netId, int error, int latencyMs,
+ String ipAddr, int port, int uid) { }
+
+ @Override
+ public void onWakeupEvent(String prefix, int uid, int ethertype,
+ int ipNextHeader, byte[] dstHw, String srcIp, String dstIp,
+ int srcPort, int dstPort, long timestampNs) { }
+
+ @Override
+ public void onTcpSocketStatsEvent(int[] networkIds, int[] sentPackets,
+ int[] lostPackets, int[] rttUs, int[] sentAckDiffMs) { }
+
+ @Override
+ public void onNat64PrefixEvent(int netId, boolean added,
+ String prefixString, int prefixLength) { }
+
+ @Override
+ public int getInterfaceVersion() {
+ return INetdEventListener.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return INetdEventListener.HASH;
+ }
+}
diff --git a/common/device/com/android/net/module/util/DeviceConfigUtils.java b/common/device/com/android/net/module/util/DeviceConfigUtils.java
index 1225aa7e..f8f250d7 100644
--- a/common/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/common/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -194,7 +194,7 @@ public final class DeviceConfigUtils {
return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not find the module name", e);
- return defaultEnabled;
+ return false;
}
}
diff --git a/common/device/com/android/net/module/util/async/OsAccess.java b/common/device/com/android/net/module/util/async/OsAccess.java
new file mode 100644
index 00000000..df0ded21
--- /dev/null
+++ b/common/device/com/android/net/module/util/async/OsAccess.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides access to all relevant OS functions..
+ *
+ * @hide
+ */
+public abstract class OsAccess {
+ /** Closes the given file, suppressing IO exceptions. */
+ public abstract void close(ParcelFileDescriptor fd);
+
+ /** Returns file name for debugging purposes. */
+ public abstract String getFileDebugName(ParcelFileDescriptor fd);
+
+ /** Returns inner FileDescriptor instance. */
+ public abstract FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd);
+
+ /**
+ * Reads available data from the given non-blocking file descriptor.
+ *
+ * Returns zero if there's no data to read at this moment.
+ * Returns -1 if the file has reached its end or the input stream has been closed.
+ * Otherwise returns the number of bytes read.
+ */
+ public abstract int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException;
+
+ /**
+ * Writes data into the given non-blocking file descriptor.
+ *
+ * Returns zero if there's no buffer space to write to at this moment.
+ * Otherwise returns the number of bytes written.
+ */
+ public abstract int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException;
+
+ public abstract long monotonicTimeMillis();
+ public abstract void setNonBlocking(FileDescriptor fd) throws IOException;
+ public abstract ParcelFileDescriptor[] pipe() throws IOException;
+
+ public abstract int poll(StructPollfd[] fds, int timeoutMs) throws IOException;
+ public abstract short getPollInMask();
+ public abstract short getPollOutMask();
+}
diff --git a/common/device/com/android/net/module/util/structs/IaPdOption.java b/common/device/com/android/net/module/util/structs/IaPdOption.java
new file mode 100644
index 00000000..5a09c40b
--- /dev/null
+++ b/common/device/com/android/net/module/util/structs/IaPdOption.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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 static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_PD;
+
+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.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA_PD option.
+ * https://tools.ietf.org/html/rfc8415. 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | OPTION_IA_PD | option-len |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | IAID (4 octets) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | T1 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | T2 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * . .
+ * . IA_PD-options .
+ * . .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class IaPdOption extends Struct {
+ public static final int LENGTH = 12; // option length excluding IA_PD options
+
+ @Field(order = 0, type = Type.S16)
+ public short code;
+ @Field(order = 1, type = Type.S16)
+ public short length;
+ @Field(order = 2, type = Type.U32)
+ public long id;
+ @Field(order = 3, type = Type.U32)
+ public long t1;
+ @Field(order = 4, type = Type.U32)
+ public long t2;
+
+ IaPdOption(final short code, final short length, final long id, final long t1,
+ final long t2) {
+ this.code = code;
+ this.length = length;
+ this.id = id;
+ this.t1 = t1;
+ this.t2 = t2;
+ }
+
+ /**
+ * Build an IA_PD option from the required specific parameters.
+ */
+ public static ByteBuffer build(final short length, final long id, final long t1,
+ final long t2) {
+ final IaPdOption option = new IaPdOption((short) DHCP6_OPTION_IA_PD,
+ length /* 12 + IA_PD options length */, id, t1, t2);
+ return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+ }
+}
diff --git a/common/device/com/android/net/module/util/structs/IaPrefixOption.java b/common/device/com/android/net/module/util/structs/IaPrefixOption.java
new file mode 100644
index 00000000..1ac21ffb
--- /dev/null
+++ b/common/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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 static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+
+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.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA Prefix Option.
+ * https://tools.ietf.org/html/rfc8415. 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | OPTION_IAPREFIX | option-len |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | preferred-lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | valid-lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | prefix-length | |
+ * +-+-+-+-+-+-+-+-+ IPv6-prefix |
+ * | (16 octets) |
+ * | |
+ * | |
+ * | |
+ * | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | | .
+ * +-+-+-+-+-+-+-+-+ .
+ * . IAprefix-options .
+ * . .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class IaPrefixOption extends Struct {
+ public static final int LENGTH = 25; // option length excluding IAprefix-options
+
+ @Field(order = 0, type = Type.S16)
+ public short code;
+ @Field(order = 1, type = Type.S16)
+ public short length;
+ @Field(order = 2, type = Type.U32)
+ public long preferred;
+ @Field(order = 3, type = Type.U32)
+ public long valid;
+ @Field(order = 4, type = Type.U8)
+ public short prefixLen;
+ @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+ public byte[] prefix;
+
+ IaPrefixOption(final short code, final short length, final long preferred,
+ final long valid, final short prefixLen, final byte[] prefix) {
+ this.code = code;
+ this.length = length;
+ this.preferred = preferred;
+ this.valid = valid;
+ this.prefixLen = prefixLen;
+ this.prefix = prefix.clone();
+ }
+
+ /**
+ * Build an IA_PD prefix option with given specific parameters.
+ */
+ public static ByteBuffer build(final short length, final long preferred, final long valid,
+ final short prefixLen, final byte[] prefix) {
+ final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX,
+ length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
+ return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+ }
+}
diff --git a/common/framework/com/android/net/module/util/NetworkStackConstants.java b/common/framework/com/android/net/module/util/NetworkStackConstants.java
index 7b3943ce..1d88d6ef 100644
--- a/common/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/common/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -214,11 +214,14 @@ public final class NetworkStackConstants {
*
* See also:
* - https://datatracker.ietf.org/doc/html/rfc8415
+ * - https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml
*/
public static final int DHCP6_CLIENT_PORT = 546;
public static final int DHCP6_SERVER_PORT = 547;
public static final Inet6Address ALL_DHCP_RELAY_AGENTS_AND_SERVERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::1:2");
+ public static final int DHCP6_OPTION_IA_PD = 25;
+ public static final int DHCP6_OPTION_IAPREFIX = 26;
/**
* IEEE802.11 standard constants.
diff --git a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 302388db..a8e7993a 100644
--- a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -223,13 +223,9 @@ public class DeviceConfigUtilsTest {
TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
}
diff --git a/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
index 46a3588f..30e0dafe 100644
--- a/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
+++ b/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -19,6 +19,7 @@ package com.android.testutils
import android.os.Handler
import android.os.HandlerThread
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -72,5 +73,9 @@ class HandlerUtilsTest {
assertEquals(attempt, x)
handler.post { assertEquals(attempt, x) }
}
+
+ assertFailsWith<IllegalArgumentException> {
+ visibleOnHandlerThread(handler) { throw IllegalArgumentException() }
+ }
}
}
diff --git a/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
index eed31e0f..ec7cdbdf 100644
--- a/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
+++ b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
@@ -34,6 +34,7 @@ import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CA
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertFails
@@ -140,20 +141,28 @@ class TestableNetworkCallbackTest {
val meteredNc = NetworkCapabilities()
val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED)
// Check that expecting caps (with or without) fails when no callback has been received.
- assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
- assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+ }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+ }
// Add NOT_METERED and check that With succeeds and Without fails.
mCallback.onCapabilitiesChanged(net, unmeteredNc)
- mCallback.expectCapabilitiesWith(NOT_METERED, matcher)
+ mCallback.expectCaps(matcher) { it.hasCapability(NOT_METERED) }
mCallback.onCapabilitiesChanged(net, unmeteredNc)
- assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+ }
// Don't add NOT_METERED and check that With fails and Without succeeds.
mCallback.onCapabilitiesChanged(net, meteredNc)
- assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+ }
mCallback.onCapabilitiesChanged(net, meteredNc)
- mCallback.expectCapabilitiesWithout(NOT_METERED, matcher)
+ mCallback.expectCaps(matcher) { !it.hasCapability(NOT_METERED) }
}
@Test
@@ -179,37 +188,35 @@ class TestableNetworkCallbackTest {
}
@Test
- fun testCapabilitiesThat() {
+ fun testExpectCaps() {
val net = Network(101)
val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI)
// Check that expecting capabilitiesThat anything fails when no callback has been received.
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { true } }
// Basic test for true and false
mCallback.onCapabilitiesChanged(net, netCaps)
- mCallback.expectCapabilitiesThat(net) { true }
+ mCallback.expectCaps(net) { true }
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { false } }
// Try a positive and a negative case
mCallback.onCapabilitiesChanged(net, netCaps)
- mCallback.expectCapabilitiesThat(net) { caps ->
- caps.hasCapability(NOT_METERED) &&
- caps.hasTransport(WIFI) &&
- !caps.hasTransport(CELLULAR)
+ mCallback.expectCaps(net) {
+ it.hasCapability(NOT_METERED) && it.hasTransport(WIFI) && !it.hasTransport(CELLULAR)
}
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps ->
- caps.hasTransport(CELLULAR)
- } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { it.hasTransport(CELLULAR) } }
// Try a matching callback on the wrong network
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } }
+ assertFails {
+ mCallback.expectCaps(Network(100), SHORT_TIMEOUT_MS) { true }
+ }
}
@Test
- fun testLinkPropertiesThat() {
+ fun testLinkPropertiesCallbacks() {
val net = Network(112)
val linkAddress = LinkAddress("fe80::ace:d00d/64")
val mtu = 1984
@@ -220,30 +227,30 @@ class TestableNetworkCallbackTest {
}
// Check that expecting linkPropsThat anything fails when no callback has been received.
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } }
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { true } }
// Basic test for true and false
mCallback.onLinkPropertiesChanged(net, linkProps)
- mCallback.expectLinkPropertiesThat(net) { true }
+ mCallback.expect<LinkPropertiesChanged>(net) { true }
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } }
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { false } }
// Try a positive and negative case
mCallback.onLinkPropertiesChanged(net, linkProps)
- mCallback.expectLinkPropertiesThat(net) { lp ->
- lp.interfaceName == TEST_INTERFACE_NAME &&
- lp.linkAddresses.contains(linkAddress) &&
- lp.mtu == mtu
+ mCallback.expect<LinkPropertiesChanged>(net) {
+ it.lp.interfaceName == TEST_INTERFACE_NAME &&
+ it.lp.linkAddresses.contains(linkAddress) &&
+ it.lp.mtu == mtu
}
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp ->
- lp.interfaceName != TEST_INTERFACE_NAME
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) {
+ it.lp.interfaceName != TEST_INTERFACE_NAME
} }
// Try a matching callback on the wrong network
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp ->
- lp.interfaceName == TEST_INTERFACE_NAME
+ assertFails { mCallback.expect<LinkPropertiesChanged>(Network(114), SHORT_TIMEOUT_MS) {
+ it.lp.interfaceName == TEST_INTERFACE_NAME
} }
}
diff --git a/common/testutils/Android.bp b/common/testutils/Android.bp
index c9b3d070..bcf89b34 100644
--- a/common/testutils/Android.bp
+++ b/common/testutils/Android.bp
@@ -36,6 +36,7 @@ java_library {
"libnanohttpd",
"net-tests-utils-host-device-common",
"net-utils-device-common",
+ "net-utils-device-common-async",
"net-utils-device-common-netlink",
"modules-utils-build_system",
],
diff --git a/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt
index 68713490..aa252a56 100644
--- a/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt
+++ b/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -65,11 +65,13 @@ fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) {
*/
fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) {
val cv = ConditionVariable()
+ var e: Exception? = null
handler.post {
try {
r.run()
} catch (exception: Exception) {
Log.e(TAG, "visibleOnHandlerThread caught exception", exception)
+ e = exception
}
cv.open()
}
@@ -77,4 +79,5 @@ fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) {
// and this thread also has seen the change (since cv.open() happens-before cv.block()
// returns).
cv.block()
+ e?.let { throw it }
}
diff --git a/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 68d5fa91..485799c1 100644
--- a/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -83,7 +83,7 @@ open class RecorderCallback private constructor(
) : CallbackEntry()
data class BlockedStatusInt(
override val network: Network,
- val blocked: Int
+ val reason: Int
) : CallbackEntry()
// Convenience constants for expecting a type
companion object {
@@ -414,20 +414,6 @@ open class TestableNetworkCallback private constructor(
crossinline predicate: (T) -> Boolean = { true }
) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
- inline fun expectCapabilitiesThat(
- net: Network,
- tmt: Long = defaultTimeoutMs,
- valid: (NetworkCapabilities) -> Boolean
- ): CapabilitiesChanged =
- expect(net, tmt, "Capabilities don't match expectations") { valid(it.caps) }
-
- inline fun expectLinkPropertiesThat(
- net: Network,
- tmt: Long = defaultTimeoutMs,
- valid: (LinkProperties) -> Boolean
- ): LinkPropertiesChanged =
- expect(net, tmt, "LinkProperties don't match expectations") { valid(it.lp) }
-
// Expects onAvailable and the callbacks that follow it. These are:
// - onSuspended, iff the network was suspended when the callbacks fire.
// - onCapabilitiesChanged.
@@ -448,18 +434,18 @@ open class TestableNetworkCallback private constructor(
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
- expectBlockedStatusCallback(blocked, net, tmt)
+ expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
}
fun expectAvailableCallbacks(
net: Network,
suspended: Boolean,
validated: Boolean,
- blockedStatus: Int,
+ blockedReason: Int,
tmt: Long
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
- expectBlockedStatusCallback(blockedStatus, net)
+ expect<BlockedStatusInt>(net) { it.reason == blockedReason }
}
private fun expectAvailableCallbacksCommon(
@@ -472,10 +458,8 @@ open class TestableNetworkCallback private constructor(
if (suspended) {
expect<Suspended>(net, tmt)
}
- expectCapabilitiesThat(net, tmt) {
- validated == null || validated == it.hasCapability(
- NET_CAPABILITY_VALIDATED
- )
+ expect<CapabilitiesChanged>(net, tmt) {
+ validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
}
expect<LinkPropertiesChanged>(net, tmt)
}
@@ -488,16 +472,6 @@ open class TestableNetworkCallback private constructor(
tmt: Long = defaultTimeoutMs
) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
- fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) =
- expect<BlockedStatus>(net, tmt, "Unexpected blocked status") {
- it.blocked == blocked
- }
-
- fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) =
- expect<BlockedStatusInt>(net, tmt, "Unexpected blocked status") {
- it.blocked == blocked
- }
-
// Expects the available callbacks (where the onCapabilitiesChanged must contain the
// VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
// one we just sent.
@@ -514,17 +488,17 @@ open class TestableNetworkCallback private constructor(
// when a network connects and satisfies a callback, and then immediately validates.
fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
expectAvailableCallbacks(net, validated = false, tmt = tmt)
- expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+ expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
fun expectAvailableThenValidatedCallbacks(
net: Network,
- blockedStatus: Int,
+ blockedReason: Int,
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacks(net, validated = false, suspended = false,
- blockedStatus = blockedStatus, tmt = tmt)
- expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+ blockedReason = blockedReason, tmt = tmt)
+ expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
// Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
@@ -571,38 +545,26 @@ open class TestableNetworkCallback private constructor(
}
@JvmOverloads
- fun expectLinkPropertiesThat(
+ fun expectCaps(
n: HasNetwork,
tmt: Long = defaultTimeoutMs,
- valid: (LinkProperties) -> Boolean
- ) = expectLinkPropertiesThat(n.network, tmt, valid)
+ valid: (NetworkCapabilities) -> Boolean = { true }
+ ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
@JvmOverloads
- fun expectCapabilitiesThat(
- n: HasNetwork,
+ fun expectCaps(
+ n: Network,
tmt: Long = defaultTimeoutMs,
valid: (NetworkCapabilities) -> Boolean
- ) = expectCapabilitiesThat(n.network, tmt, valid)
+ ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
- @JvmOverloads
- fun expectCapabilitiesWith(
- capability: Int,
- n: HasNetwork,
- timeoutMs: Long = defaultTimeoutMs
- ): NetworkCapabilities {
- return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps
- }
-
- @JvmOverloads
- fun expectCapabilitiesWithout(
- capability: Int,
+ fun expectCaps(
n: HasNetwork,
- timeoutMs: Long = defaultTimeoutMs
- ): NetworkCapabilities {
- return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps
- }
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
- fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
- expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
- }
+ fun expectCaps(
+ tmt: Long,
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
}
diff --git a/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 00000000..1b8e26b3
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 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.testutils;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+ public static final boolean ENABLE_FINE_DEBUG = true;
+
+ public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+ private enum FileType { PAIR, PIPE }
+
+ // Common poll() constants:
+ private static final short POLLIN = 0x0001;
+ private static final short POLLOUT = 0x0004;
+ private static final short POLLERR = 0x0008;
+ private static final short POLLHUP = 0x0010;
+
+ private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+ private static final Field FD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_GUARD;
+ private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+ private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final HashMap<Integer, File> mFiles = new HashMap<>();
+ private final byte[] mTmpBuffer = new byte[1024];
+ private final long mStartTime;
+ private final String mLogTag;
+ private int mFileNumberGen = 3;
+ private boolean mHasRateLimitedData;
+
+ public FakeOsAccess(String logTag) {
+ mLogTag = logTag;
+ mStartTime = monotonicTimeMillis();
+ }
+
+ @Override
+ public long monotonicTimeMillis() {
+ return System.nanoTime() / 1000000;
+ }
+
+ @Override
+ public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+ try {
+ return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(ParcelFileDescriptor fd) {
+ if (fd != null) {
+ close(getInnerFileDescriptor(fd));
+
+ try {
+ // Reduce CloseGuard warnings.
+ Object guard = PFD_FIELD_GUARD.get(fd);
+ CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void close(FileDescriptor fd) {
+ if (fd != null) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.decreaseRefCount();
+ mFiles.remove(getFileDescriptorNumber(fd));
+ setFileDescriptorNumber(fd, -1);
+ notifyAll();
+ }
+ }
+ }
+
+ private File getFile(String func, FileDescriptor fd) throws IOException {
+ File file = getFileOrNull(fd);
+ if (file == null) {
+ throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+ }
+ return file;
+ }
+
+ private File getFileOrNull(FileDescriptor fd) {
+ return mFiles.get(getFileDescriptorNumber(fd));
+ }
+
+ @Override
+ public String getFileDebugName(ParcelFileDescriptor fd) {
+ return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+ }
+
+ public String getFileDebugName(FileDescriptor fd) {
+ if (fd == null) {
+ return "null";
+ }
+
+ final int fdNumber = getFileDescriptorNumber(fd);
+ File file = mFiles.get(fdNumber);
+
+ StringBuilder sb = new StringBuilder();
+ if (file != null) {
+ if (file.name != null) {
+ sb.append(file.name);
+ sb.append("/");
+ }
+ sb.append(file.type);
+ sb.append("/");
+ } else {
+ sb.append("BADFD/");
+ }
+ sb.append(fdNumber);
+ return sb.toString();
+ }
+
+ public synchronized void setFileName(FileDescriptor fd, String name) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.name = name;
+ }
+ }
+
+ @Override
+ public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+ File file = getFile("fcntl", fd);
+ file.isBlocking = false;
+ }
+
+ @Override
+ public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("read", buffer, pos, len);
+
+ File file = getFile("read", fd);
+ if (file.readQueue == null) {
+ throw newIOException("read", "File not readable");
+ }
+ file.checkNonBlocking("read");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int availSize = file.readQueue.size();
+ if (availSize == 0) {
+ if (file.isEndOfStream) {
+ // Java convention uses -1 to indicate end of stream.
+ return -1;
+ }
+ return 0; // EAGAIN
+ }
+
+ final int readCount = Math.min(len, availSize);
+ file.readQueue.readBytes(buffer, pos, readCount);
+ maybeTransferData(file);
+ return readCount;
+ }
+
+ @Override
+ public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("write", buffer, pos, len);
+
+ File file = getFile("write", fd);
+ if (file.writeQueue == null) {
+ throw newIOException("read", "File not writable");
+ }
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ throw newIOException("write", "The other end of pipe is closed");
+ }
+ file.checkNonBlocking("write");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int originalFreeSize = file.writeQueue.freeSize();
+ if (originalFreeSize == 0) {
+ return 0; // EAGAIN
+ }
+
+ final int writeCount = Math.min(len, originalFreeSize);
+ file.writeQueue.writeBytes(buffer, pos, writeCount);
+ maybeTransferData(file);
+
+ if (file.writeQueue.freeSize() < originalFreeSize) {
+ final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+ Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+ + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+ + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+ }
+
+ return writeCount;
+ }
+
+ private void maybeTransferData(File file) {
+ boolean hasChanges = copyFileBuffers(file, file.sink);
+ hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+ if (hasChanges) {
+ // TODO(b/245971639): Avoid notifying if no-one is polling.
+ notifyAll();
+ }
+ }
+
+ private boolean copyFileBuffers(File src, File dst) {
+ if (src.writeQueue == null || dst.readQueue == null) {
+ return false;
+ }
+
+ final int originalCopyCount = Math.min(mTmpBuffer.length,
+ Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+ final int allowedCopyCount = RateLimiter.limit(
+ src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+ if (allowedCopyCount < originalCopyCount) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.i(mLogTag, logStr("Delaying transfer of "
+ + (originalCopyCount - allowedCopyCount) + " bytes, original="
+ + originalCopyCount + ", allowed=" + allowedCopyCount
+ + ", type=" + src.type));
+ }
+ if (originalCopyCount > 0) {
+ mHasRateLimitedData = true;
+ }
+ if (allowedCopyCount == 0) {
+ return false;
+ }
+ }
+
+ boolean hasChanges = false;
+ if (allowedCopyCount > 0) {
+ if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+ hasChanges = true; // Read queue had no data, or write queue was full.
+ }
+ src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+ dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+ }
+
+ if (!dst.isEndOfStream && src.openCount == 0
+ && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+ dst.isEndOfStream = true;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ public void clearInboundRateLimit(FileDescriptor fd) {
+ setInboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public void clearOutboundRateLimit(FileDescriptor fd) {
+ setOutboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+ int fdNumber1 = getNextFd("socketpair");
+ int fdNumber2 = getNextFd("socketpair");
+
+ File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+ File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ @Override
+ public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+ int fdNumber1 = getNextFd("pipe");
+ int fdNumber2 = getNextFd("pipe");
+
+ File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+ File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ private ParcelFileDescriptor[] registerFilePair(
+ int fdNumber1, File file1, int fdNumber2, File file2) {
+ file1.sink = file2;
+ file1.source = file2;
+ file2.sink = file1;
+ file2.source = file1;
+
+ mFiles.put(fdNumber1, file1);
+ mFiles.put(fdNumber2, file2);
+ return new ParcelFileDescriptor[] {
+ newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+ }
+
+ @Override
+ public short getPollInMask() {
+ return POLLIN;
+ }
+
+ @Override
+ public short getPollOutMask() {
+ return POLLOUT;
+ }
+
+ @Override
+ public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+ if (timeoutMs < 0) {
+ timeoutMs = (int) TimeUnit.HOURS.toMillis(1); // Make "infinite" equal to 1 hour.
+ }
+
+ if (fds == null || fds.length > 1000) {
+ throw newIOException("poll", "Invalid fds param");
+ }
+ for (StructPollfd pollFd : fds) {
+ getFile("poll", pollFd.fd);
+ }
+
+ int waitCallCount = 0;
+ final long deadline = monotonicTimeMillis() + timeoutMs;
+ while (true) {
+ if (mHasRateLimitedData) {
+ mHasRateLimitedData = false;
+ for (File file : mFiles.values()) {
+ if (file.inboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file.source, file);
+ }
+ if (file.outboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file, file.sink);
+ }
+ }
+ }
+
+ final int readyCount = calculateReadyCount(fds);
+ if (readyCount > 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll returns " + readyCount
+ + " after " + waitCallCount + " wait calls"));
+ }
+ return readyCount;
+ }
+
+ long remainingTimeoutMs = deadline - monotonicTimeMillis();
+ if (remainingTimeoutMs <= 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+ + "ms after " + waitCallCount + " wait calls"));
+ }
+ return 0;
+ }
+
+ if (mHasRateLimitedData) {
+ remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+ }
+
+ try {
+ wait(remainingTimeoutMs);
+ } catch (InterruptedException e) {
+ // Ignore and retry
+ }
+ waitCallCount++;
+ }
+ }
+
+ private int calculateReadyCount(StructPollfd[] fds) {
+ int fdCount = 0;
+ for (StructPollfd pollFd : fds) {
+ pollFd.revents = 0;
+
+ File file = getFileOrNull(pollFd.fd);
+ if (file == null) {
+ Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+ + getFileDebugName(pollFd.fd)));
+ continue;
+ }
+
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+ + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+ + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+ + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+ }
+
+ if ((pollFd.events & POLLIN) != 0) {
+ if (file.readQueue != null && file.readQueue.size() != 0) {
+ pollFd.revents |= POLLIN;
+ }
+ if (file.isEndOfStream) {
+ pollFd.revents |= POLLHUP;
+ }
+ }
+
+ if ((pollFd.events & POLLOUT) != 0) {
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ pollFd.revents |= POLLERR;
+ }
+ if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+ pollFd.revents |= POLLOUT;
+ }
+ }
+
+ if (pollFd.revents != 0) {
+ fdCount++;
+ }
+ }
+ return fdCount;
+ }
+
+ private int getNextFd(String func) throws IOException {
+ if (mFileNumberGen > 100000) {
+ throw newIOException(func, "Too many files open");
+ }
+
+ return mFileNumberGen++;
+ }
+
+ private static IOException newIOException(String func, String message) {
+ return new IOException(message + ", func=" + func);
+ }
+
+ public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+ throws IOException {
+ if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+ throw newIOException(func, "Invalid array bounds");
+ }
+ }
+
+ private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+ try {
+ return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private FileDescriptor newFileDescriptor(int fdNumber) {
+ try {
+ return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getFileDescriptorNumber(FileDescriptor fd) {
+ try {
+ return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+ try {
+ FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String logStr(String message) {
+ return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+ }
+
+ private class File {
+ final FileType type;
+ final CircularByteBuffer readQueue;
+ final CircularByteBuffer writeQueue;
+ final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ String name;
+ int openCount = 1;
+ boolean isBlocking = true;
+ File sink;
+ File source;
+ boolean isEndOfStream;
+
+ File(FileType type, int readQueueSize, int writeQueueSize) {
+ this.type = type;
+ readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+ writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+ }
+
+ void decreaseRefCount() {
+ if (openCount <= 0) {
+ throw new IllegalStateException();
+ }
+ openCount--;
+ }
+
+ void checkNonBlocking(String func) throws IOException {
+ if (isBlocking) {
+ throw newIOException(func, "File in blocking mode");
+ }
+ }
+ }
+
+ static {
+ try {
+ FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+ FD_CONSTRUCTOR.setAccessible(true);
+
+ Field descriptorIntField;
+ try {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+ } catch (NoSuchFieldException e) {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+ }
+ FD_FIELD_DESCRIPTOR = descriptorIntField;
+ FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+ PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+ PFD_FIELD_GUARD.setAccessible(true);
+
+ CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+ .getDeclaredMethod("close");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 00000000..137873db
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 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.testutils;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+ private static final int PERIOD_DURATION_MS = 1000;
+ private static final int BUCKET_COUNT = 100;
+
+ public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+ private final OsAccess mOsAccess;
+ private final int[] mStatBuckets = new int[BUCKET_COUNT];
+ private int mMaxPerPeriodBytes;
+ private int mMaxPerBucketBytes;
+ private int mRecordedPeriodBytes;
+ private long mLastLimitTimestamp;
+ private int mLastRequestReduction;
+
+ public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+ mOsAccess = osAccess;
+ setBytesPerSecond(bytesPerSecond);
+ clear();
+ }
+
+ public int getBytesPerSecond() {
+ return mMaxPerPeriodBytes;
+ }
+
+ public void setBytesPerSecond(int bytesPerSecond) {
+ mMaxPerPeriodBytes = bytesPerSecond;
+ mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+ }
+
+ public void clear() {
+ mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+ mRecordedPeriodBytes = 0;
+ Arrays.fill(mStatBuckets, 0);
+ }
+
+ public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+ final long now = limiter1.mOsAccess.monotonicTimeMillis();
+ final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+ limiter2.calculateLimit(now, requestedBytes));
+ limiter1.recordBytes(now, requestedBytes, allowedCount);
+ limiter2.recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int limit(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int getLastRequestReduction() {
+ return mLastRequestReduction;
+ }
+
+ public boolean acceptAllOrNone(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ if (allowedCount < requestedBytes) {
+ return false;
+ }
+ recordBytes(now, requestedBytes, allowedCount);
+ return true;
+ }
+
+ private int calculateLimit(long now, int requestedBytes) {
+ // First remove all stale bucket data and adjust the total.
+ final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+ final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+ for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+ final int idx = (int) (i % BUCKET_COUNT);
+ mRecordedPeriodBytes -= mStatBuckets[idx];
+ mStatBuckets[idx] = 0;
+ }
+
+ final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+ final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+ Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+ return Math.max(0, maxAllowed);
+ }
+
+ private void recordBytes(long now, int requestedBytes, int actualBytes) {
+ mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+ mRecordedPeriodBytes += actualBytes;
+ mLastRequestReduction = requestedBytes - actualBytes;
+ mLastLimitTimestamp = now;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{max=");
+ sb.append(mMaxPerPeriodBytes);
+ sb.append(",max_bucket=");
+ sb.append(mMaxPerBucketBytes);
+ sb.append(",total=");
+ sb.append(mRecordedPeriodBytes);
+ sb.append(",last_red=");
+ sb.append(mLastRequestReduction);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
new file mode 100644
index 00000000..5af890ff
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String)