[Thread] add E2E UDP test for app usage

Bug: 323300829
Bug: 325983649
Change-Id: I0e6b9029cb58a2b985e80003d6339e536d706ed3
This commit is contained in:
Kangping Dong 2024-04-02 18:01:13 +08:00
commit e9daad6264
3 changed files with 119 additions and 12 deletions

View file

@ -29,6 +29,7 @@ import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
@ -45,14 +46,25 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
@RequiresThreadFeature
@RunWith(AndroidJUnit4.class)
public class ThreadIntegrationTest {
// The byte[] buffer size for UDP tests
private static final int UDP_BUFFER_SIZE = 1024;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@ -66,24 +78,32 @@ public class ThreadIntegrationTest {
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
private ExecutorService mExecutor;
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
private OtDaemonController mOtCtl;
private FullThreadDevice mFtd;
@Before
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
mOtCtl = new OtDaemonController();
mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl.factoryReset();
mFtd = new FullThreadDevice(10 /* nodeId */);
}
@After
public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
mFtd.destroy();
mExecutor.shutdownNow();
}
@Test
@ -153,10 +173,57 @@ public class ThreadIntegrationTest {
assertThat(mOtCtl.getCountryCode()).isEqualTo("CN");
}
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
@Test
public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
// Topology:
// Test App ------ thread-wpan ------ End Device
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
final Inet6Address serverAddress = mOtCtl.getMeshLocalAddresses().get(0);
final int serverPort = 9527;
mExecutor.execute(() -> startUdpEchoServerAndWait(serverAddress, serverPort));
mFtd.udpOpen();
mFtd.udpSend("Hello,Thread", serverAddress, serverPort);
String udpReply = mFtd.udpReceive();
assertThat(udpReply).isEqualTo("Hello,Thread");
}
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
}
private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
throws Exception {
ftd.factoryReset();
ftd.joinNetwork(activeDataset);
ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
}
/**
* Starts a UDP echo server and replies to the first UDP message.
*
* <p>This method exits when the first UDP message is received and the reply is sent
*/
private void startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort) {
try (var udpServerSocket = new DatagramSocket(serverPort, serverAddress)) {
DatagramPacket recvPacket =
new DatagramPacket(new byte[UDP_BUFFER_SIZE], UDP_BUFFER_SIZE);
udpServerSocket.receive(recvPacket);
byte[] sendBuffer = Arrays.copyOf(recvPacket.getData(), recvPacket.getData().length);
udpServerSocket.send(
new DatagramPacket(
sendBuffer,
sendBuffer.length,
(Inet6Address) recvPacket.getAddress(),
recvPacket.getPort()));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}

View file

@ -20,8 +20,6 @@ import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.google.common.io.BaseEncoding.base16;
import static org.junit.Assert.fail;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.nsd.NsdServiceInfo;
@ -218,6 +216,11 @@ public final class FullThreadDevice {
return matcher.group(4);
}
/** Sends a UDP message to given IPv6 address and port. */
public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
}
/** Enables the SRP client and run in autostart mode. */
public void autoStartSrpClient() {
executeCommand("srp client autostart enable");
@ -474,7 +477,7 @@ public final class FullThreadDevice {
break;
}
if (line.startsWith("Error")) {
fail("ot-cli-ftd reported an error: " + line);
throw new IOException("ot-cli-ftd reported an error: " + line);
}
if (!line.startsWith("> ")) {
result.add(line);

View file

@ -16,13 +16,16 @@
package android.net.thread.utils;
import android.annotation.Nullable;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.os.SystemClock;
import com.android.compatibility.common.util.SystemUtil;
import java.net.Inet6Address;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
@ -53,10 +56,7 @@ public final class OtDaemonController {
/** Returns the list of IPv6 addresses on ot-daemon. */
public List<Inet6Address> getAddresses() {
String output = executeCommand("ipaddr");
return Arrays.asList(output.split("\n")).stream()
.map(String::trim)
.filter(str -> !str.equals("Done"))
return executeCommandAndParse("ipaddr").stream()
.map(addr -> InetAddresses.parseNumericAddress(addr))
.map(inetAddr -> (Inet6Address) inetAddr)
.toList();
@ -70,17 +70,54 @@ public final class OtDaemonController {
/** Returns the ML-EID of the device. */
public Inet6Address getMlEid() {
String addressStr = executeCommand("ipaddr mleid").split("\n")[0].trim();
String addressStr = executeCommandAndParse("ipaddr mleid").get(0);
return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
}
/** Returns the country code on ot-daemon. */
public String getCountryCode() {
String countryCodeStr = executeCommand("region").split("\n")[0].trim();
return countryCodeStr;
return executeCommandAndParse("region").get(0);
}
/**
* Returns the list of IPv6 Mesh-Local addresses on ot-daemon.
*
* <p>The return List can be empty if no Mesh-Local prefix exists.
*/
public List<Inet6Address> getMeshLocalAddresses() {
IpPrefix meshLocalPrefix = getMeshLocalPrefix();
if (meshLocalPrefix == null) {
return Collections.emptyList();
}
return getAddresses().stream().filter(addr -> meshLocalPrefix.contains(addr)).toList();
}
/**
* Returns the Mesh-Local prefix or {@code null} if none exists (e.g. the Active Dataset is not
* set).
*/
@Nullable
public IpPrefix getMeshLocalPrefix() {
List<IpPrefix> prefixes =
executeCommandAndParse("prefix meshlocal").stream()
.map(prefix -> new IpPrefix(prefix))
.toList();
return prefixes.isEmpty() ? null : prefixes.get(0);
}
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
/**
* Executes a ot-ctl command and parse the output to a list of strings.
*
* <p>The trailing "Done" in the command output will be dropped.
*/
public List<String> executeCommandAndParse(String cmd) {
return Arrays.asList(executeCommand(cmd).split("\n")).stream()
.map(String::trim)
.filter(str -> !str.equals("Done"))
.toList();
}
}