Merge "[Thread] add Thread Operational Dataset API" into main

This commit is contained in:
Kangping Dong
2023-10-25 12:24:16 +00:00
committed by Gerrit Code Review
16 changed files with 3214 additions and 0 deletions

View File

@@ -417,6 +417,81 @@ package android.net.nsd {
package android.net.thread {
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset();
method public int describeContents();
method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
method @IntRange(from=0, to=65535) public int getChannel();
method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
method @IntRange(from=0, to=255) public int getChannelPage();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
method @IntRange(from=0, to=65534) public int getPanId();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
method @NonNull public byte[] toThreadTlvs();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
field public static final int LENGTH_PSKC = 16; // 0x10
}
public static final class ActiveOperationalDataset.Builder {
ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
ctor public ActiveOperationalDataset.Builder();
method @NonNull public android.net.thread.ActiveOperationalDataset build();
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
}
public static final class ActiveOperationalDataset.SecurityPolicy {
ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
method @IntRange(from=1, to=65535) public int getRotationTimeHours();
field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
method @IntRange(from=0, to=281474976710655L) public long getSeconds();
method @IntRange(from=0, to=32767) public int getTicks();
method public boolean isAuthoritativeSource();
method @NonNull public java.time.Instant toInstant();
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
method public int describeContents();
method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
method @NonNull public java.time.Duration getDelayTimer();
method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
method @NonNull public byte[] toThreadTlvs();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkController {
method public int getThreadVersion();
field public static final int THREAD_VERSION_1_3 = 4; // 0x4

View File

@@ -0,0 +1,19 @@
/*
* Copyright 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 android.net.thread;
parcelable ActiveOperationalDataset;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,220 @@
/*
* 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 android.net.thread;
import static com.android.internal.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Objects;
/**
* The timestamp of Thread Operational Dataset.
*
* @see ActiveOperationalDataset
* @see PendingOperationalDataset
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class OperationalDatasetTimestamp {
/** @hide */
public static final int LENGTH_TIMESTAMP = Long.BYTES;
private static final long TICKS_UPPER_BOUND = 0x8000;
private final Instant mInstant;
private final boolean mIsAuthoritativeSource;
/**
* Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
*
* <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
* {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
* is set to {@code true}.
*
* @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
* 0xffffffffffffL}
*/
@NonNull
public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true);
}
/** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */
@NonNull
public Instant toInstant() {
return mInstant;
}
/**
* Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
* TLV value.
*
* @hide
*/
@NonNull
public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
checkArgument(
encodedTimestamp.length == LENGTH_TIMESTAMP,
"Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+ " expectedLength=%d)",
encodedTimestamp.length,
LENGTH_TIMESTAMP);
long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
return new OperationalDatasetTimestamp(
(longTimestamp >> 16) & 0x0000ffffffffffffL,
(int) ((longTimestamp >> 1) & 0x7fffL),
(longTimestamp & 0x01) != 0);
}
/**
* Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
*
* @hide
*/
@NonNull
public byte[] toTlvValue() {
byte[] tlv = new byte[LENGTH_TIMESTAMP];
ByteBuffer buffer = ByteBuffer.wrap(tlv);
long encodedValue =
(mInstant.getEpochSecond() << 16)
| ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1)
| (mIsAuthoritativeSource ? 1 : 0);
buffer.putLong(encodedValue);
return tlv;
}
/**
* Creates a new {@link OperationalDatasetTimestamp} object.
*
* @param seconds the value encodes a Unix Time value. Must be in the range of
* 0x0-0xffffffffffffL
* @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
* be in the range of 0x0-0x7fff
* @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
* source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
* network, or other method
* @throws IllegalArgumentException if the {@code seconds} is not in range of
* 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
*/
public OperationalDatasetTimestamp(
@IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
@IntRange(from = 0x0, to = 0x7fff) int ticks,
boolean isAuthoritativeSource) {
this(makeInstant(seconds, ticks), isAuthoritativeSource);
}
private static Instant makeInstant(long seconds, int ticks) {
checkArgument(
seconds >= 0 && seconds <= 0xffffffffffffL,
"seconds exceeds allowed range (seconds = %d,"
+ " allowedRange = [0x0, 0xffffffffffffL])",
seconds);
checkArgument(
ticks >= 0 && ticks <= 0x7fff,
"ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
ticks);
long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND);
return Instant.ofEpochSecond(seconds, nanos);
}
/**
* Creates new {@link OperationalDatasetTimestamp} object.
*
* @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
* 0xffffffffffffL}
*/
private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) {
requireNonNull(instant, "instant cannot be null");
long seconds = instant.getEpochSecond();
checkArgument(
seconds >= 0 && seconds <= 0xffffffffffffL,
"instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0,"
+ " 0xffffffffffffL])",
seconds);
mInstant = instant;
mIsAuthoritativeSource = isAuthoritativeSource;
}
/**
* Returns the rounded ticks converted from the nano seconds.
*
* <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
*/
private static int getRoundedTicks(long nanos) {
return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
}
/** Returns the seconds portion of the timestamp. */
public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND;
}
/** Returns the ticks portion of the timestamp. */
public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
// the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742
return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND);
}
/** Returns {@code true} if the timestamp comes from an authoritative source. */
public boolean isAuthoritativeSource() {
return mIsAuthoritativeSource;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{seconds=")
.append(getSeconds())
.append(", ticks=")
.append(getTicks())
.append(", isAuthoritativeSource=")
.append(isAuthoritativeSource())
.append(", instant=")
.append(toInstant())
.append("}");
return sb.toString();
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (!(other instanceof OperationalDatasetTimestamp)) {
return false;
} else {
OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
return mInstant.equals(otherTimestamp.mInstant)
&& mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
}
}
@Override
public int hashCode() {
return Objects.hash(mInstant, mIsAuthoritativeSource);
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 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 android.net.thread;
parcelable PendingOperationalDataset;

View File

@@ -0,0 +1,226 @@
/*
* 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 android.net.thread;
import static com.android.internal.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Objects;
/**
* Data interface for managing a Thread Pending Operational Dataset.
*
* <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
* a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
* Channel) to all devices in the network.
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class PendingOperationalDataset implements Parcelable {
// Value defined in Thread spec 8.10.1.16
private static final int TYPE_PENDING_TIMESTAMP = 51;
// Values defined in Thread spec 8.10.1.17
private static final int TYPE_DELAY_TIMER = 52;
private static final int LENGTH_DELAY_TIMER_BYTES = 4;
@NonNull
public static final Creator<PendingOperationalDataset> CREATOR =
new Creator<>() {
@Override
public PendingOperationalDataset createFromParcel(Parcel in) {
return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
}
@Override
public PendingOperationalDataset[] newArray(int size) {
return new PendingOperationalDataset[size];
}
};
@NonNull private final ActiveOperationalDataset mActiveOpDataset;
@NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
@NonNull private final Duration mDelayTimer;
/** Creates a new {@link PendingOperationalDataset} object. */
public PendingOperationalDataset(
@NonNull ActiveOperationalDataset activeOpDataset,
@NonNull OperationalDatasetTimestamp pendingTimestamp,
@NonNull Duration delayTimer) {
requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
requireNonNull(delayTimer, "delayTimer cannot be null");
this.mActiveOpDataset = activeOpDataset;
this.mPendingTimestamp = pendingTimestamp;
this.mDelayTimer = delayTimer;
}
/**
* Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
*
* <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
* (see the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
*
* @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
* TLV
*/
@NonNull
public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
requireNonNull(tlvs, "tlvs cannot be null");
SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
OperationalDatasetTimestamp pendingTimestamp = null;
Duration delayTimer = null;
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
for (int i = 0; i < unknownTlvs.size(); i++) {
int key = unknownTlvs.keyAt(i);
byte[] value = unknownTlvs.valueAt(i);
switch (key) {
case TYPE_PENDING_TIMESTAMP:
pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
break;
case TYPE_DELAY_TIMER:
checkArgument(
value.length == LENGTH_DELAY_TIMER_BYTES,
"Invalid delay timer (length = %d, expectedLength = %d)",
value.length,
LENGTH_DELAY_TIMER_BYTES);
int millis = ByteBuffer.wrap(value).getInt();
delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
break;
default:
newUnknownTlvs.put(key, value);
break;
}
}
if (pendingTimestamp == null) {
throw new IllegalArgumentException("Pending Timestamp is missing");
}
if (delayTimer == null) {
throw new IllegalArgumentException("Delay Timer is missing");
}
activeDataset =
new ActiveOperationalDataset.Builder(activeDataset)
.setUnknownTlvs(newUnknownTlvs)
.build();
return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
}
/** Returns the Active Operational Dataset. */
@NonNull
public ActiveOperationalDataset getActiveOperationalDataset() {
return mActiveOpDataset;
}
/** Returns the Pending Timestamp. */
@NonNull
public OperationalDatasetTimestamp getPendingTimestamp() {
return mPendingTimestamp;
}
/** Returns the Delay Timer. */
@NonNull
public Duration getDelayTimer() {
return mDelayTimer;
}
/**
* Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
*
* <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition of the Thread TLV format.
*/
@NonNull
public byte[] toThreadTlvs() {
ByteArrayOutputStream dataset = new ByteArrayOutputStream();
byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
dataset.write(TYPE_PENDING_TIMESTAMP);
byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
dataset.write(pendingTimestampBytes.length);
dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
dataset.write(TYPE_DELAY_TIMER);
byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
dataset.write(delayTimerBytes.length);
dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
return dataset.toByteArray();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof PendingOperationalDataset)) {
return false;
} else {
PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
&& mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
&& mDelayTimer.equals(otherDataset.mDelayTimer);
}
}
@Override
public int hashCode() {
return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{activeDataset=")
.append(getActiveOperationalDataset())
.append(", pendingTimestamp=")
.append(getPendingTimestamp())
.append(", delayTimer=")
.append(getDelayTimer())
.append("}");
return sb.toString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeByteArray(toThreadTlvs());
}
}

View File

@@ -37,6 +37,7 @@ android_test {
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
"guava-android-testlib",
"net-tests-utils",
"truth",
],

View File

@@ -47,5 +47,7 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.cts" />
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
</configuration>

View File

@@ -0,0 +1,737 @@
/*
* 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 android.net.thread.cts;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.Builder;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.OperationalDatasetTimestamp;
import android.util.SparseArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.primitives.Bytes;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/** CTS tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class ActiveOperationalDatasetTest {
private static final int TYPE_ACTIVE_TIMESTAMP = 14;
private static final int TYPE_CHANNEL = 0;
private static final int TYPE_CHANNEL_MASK = 53;
private static final int TYPE_EXTENDED_PAN_ID = 2;
private static final int TYPE_MESH_LOCAL_PREFIX = 7;
private static final int TYPE_NETWORK_KEY = 5;
private static final int TYPE_NETWORK_NAME = 3;
private static final int TYPE_PAN_ID = 1;
private static final int TYPE_PSKC = 4;
private static final int TYPE_SECURITY_POLICY = 12;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 19
// Channel Mask: 0x07FFF800
// Ext PAN ID: ACC214689BC40BDF
// Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
// Network Key: F26B3153760F519A63BAFDDFFC80D2AF
// Network Name: OpenThread-d9a0
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
private static final byte[] VALID_DATASET =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
private static byte[] removeTlv(byte[] dataset, int type) {
ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
int i = 0;
while (i < dataset.length) {
int ty = dataset[i++] & 0xff;
byte length = dataset[i++];
if (ty != type) {
byte[] value = Arrays.copyOfRange(dataset, i, i + length);
os.write(ty);
os.write(length);
os.writeBytes(value);
}
i += length;
}
return os.toByteArray();
}
private static byte[] addTlv(byte[] dataset, String tlvHex) {
return Bytes.concat(dataset, base16().decode(tlvHex));
}
private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
return addTlv(removeTlv(dataset, type), newTlvHex);
}
@Test
public void parcelable_parcelingIsLossLess() {
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
assertParcelingIsLossless(dataset);
}
@Test
public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
byte[] invalidTlv = new byte[255];
invalidTlv[0] = (byte) 0xff;
// This is invalid because the TLV has max total length of 254 bytes and the value length
// can't exceeds 252 ( = 254 - 1 - 1)
invalidTlv[1] = (byte) 253;
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(
VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_undefinedChannelPage_success() {
byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
assertThat(dataset.getChannelPage()).isEqualTo(0x01);
assertThat(dataset.getChannel()).isEqualTo(0x20);
}
@Test
public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A");
byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
}
@Test
public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
assertThat(dataset.getChannel()).isEqualTo(16);
}
@Test
public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(
replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000"));
SparseArray<byte[]> channelMask = dataset.getChannelMask();
assertThat(channelMask.size()).isEqualTo(1);
assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
.isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
}
@Test
public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
}
@Test
public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
}
@Test
public void fromThreadTlvs_validFullDataset_success() {
// A valid Thread active operational dataset:
// Active Timestamp: 1
// Channel: 19
// Channel Mask: 0x07FFF800
// Ext PAN ID: ACC214689BC40BDF
// Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
// Network Key: F26B3153760F519A63BAFDDFFC80D2AF
// Network Name: OpenThread-d9a0
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
byte[] validDatasetTlv =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
assertThat(dataset.getNetworkKey())
.isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
assertThat(dataset.getChannel()).isEqualTo(19);
assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
assertThat(dataset.getPskc())
.isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
assertThat(dataset.getActiveTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
SparseArray<byte[]> channelMask = dataset.getChannelMask();
assertThat(channelMask.size()).isEqualTo(1);
assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
.isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
assertThat(dataset.getMeshLocalPrefix())
.isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
assertThat(dataset.getSecurityPolicy())
.isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
}
@Test
public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
byte[] newDatasetTlvs = dataset.toThreadTlvs();
String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
assertThat(newDatasetTlvsHex).contains("AA01FF");
assertThat(newDatasetTlvsHex).contains("BB020102");
}
@Test
public void toThreadTlvs_conversionIsLossLess() {
ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset();
ActiveOperationalDataset dataset2 =
ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
assertThat(dataset2).isEqualTo(dataset1);
}
@Test
public void builder_buildWithdefaultValues_throwsIllegalState() {
assertThrows(IllegalStateException.class, () -> new Builder().build());
}
@Test
public void builder_setValidNetworkKey_success() {
final byte[] networkKey =
new byte[] {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f
};
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkKey(networkKey)
.build();
assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
}
@Test
public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
}
@Test
public void builder_setValidExtendedPanId_success() {
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setExtendedPanId(extendedPanId)
.build();
assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
}
@Test
public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
}
@Test
public void builder_setValidPanId_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setPanId(0xfffe)
.build();
assertThat(dataset.getPanId()).isEqualTo(0xfffe);
}
@Test
public void builder_setInvalidPanId_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
}
@Test
public void builder_setInvalidChannel_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
}
@Test
public void builder_setValid2P4GhzChannel_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setChannel(CHANNEL_PAGE_24_GHZ, 16)
.build();
assertThat(dataset.getChannel()).isEqualTo(16);
assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
}
@Test
public void builder_setValidNetworkName_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkName("ot-network")
.build();
assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
}
@Test
public void builder_setEmptyNetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
}
@Test
public void builder_setTooLongNetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
}
@Test
public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
// UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
}
@Test
public void builder_setValidUtf8NetworkName_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkName("我的网络")
.build();
assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
}
@Test
public void builder_setValidPskc_success() {
byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build();
assertThat(dataset.getPskc()).isEqualTo(pskc);
}
@Test
public void builder_setTooLongPskc_throwsIllegalArgument() {
byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
}
@Test
public void builder_setValidChannelMask_success() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build();
SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
assertThat(resultChannelMask.size()).isEqualTo(1);
assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
}
@Test
public void builder_setEmptyChannelMask_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class,
() -> builder.setChannelMask(new SparseArray<byte[]>()));
}
@Test
public void builder_setValidActiveTimestamp_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setActiveTimestamp(
new OperationalDatasetTimestamp(
/* seconds= */ 1,
/* ticks= */ 0,
/* isAuthoritativeSource= */ true))
.build();
assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
}
@Test
public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
// The Mesh-Local Prefix length must be 64 bits
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
// The Mesh-Local Prefix must start with 0xfd
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
}
@Test
public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
}
@Test
public void builder_setValidMeshLocalPrefix_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setMeshLocalPrefix(new IpPrefix("fd00::/64"))
.build();
assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
}
@Test
public void builder_setValid1P2SecurityPolicy_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setSecurityPolicy(
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
.build();
assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(dataset.getSecurityPolicy().getFlags())
.isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
}
@Test
public void builder_setValid1P1SecurityPolicy_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
.build();
assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
}
@Test
public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
assertThrows(
IllegalArgumentException.class,
() -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
}
@Test
public void securityPolicy_emptyFlags_throwsIllegalArguments() {
assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
}
@Test
public void securityPolicy_tooLongFlags_success() {
SecurityPolicy securityPolicy =
new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
}
@Test
public void securityPolicy_equals() {
new EqualsTester()
.addEqualityGroup(
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
.addEqualityGroup(
new SecurityPolicy(1, new byte[] {(byte) 0xff}),
new SecurityPolicy(1, new byte[] {(byte) 0xff}))
.addEqualityGroup(
new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
.testEquals();
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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 android.net.thread.cts;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.thread.OperationalDatasetTimestamp;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
/** Tests for {@link OperationalDatasetTimestamp}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class OperationalDatasetTimestampTest {
@Test
public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() ->
OperationalDatasetTimestamp.fromInstant(
Instant.ofEpochSecond(0xffffffffffffL + 1L)));
}
@Test
public void fromInstant_ticksIsRounded() {
Instant instant = Instant.ofEpochSecond(100L);
// 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
// the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
OperationalDatasetTimestamp timestampTicks32767 =
OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
OperationalDatasetTimestamp timestampTicks0 =
OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
assertThat(timestampTicks0.getTicks()).isEqualTo(0);
assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
}
@Test
public void toInstant_nanosIsRounded() {
// 32767 / 32768 * 1000000000 = 999969482.421875
assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
.isEqualTo(999969482);
// 32766 / 32768 * 1000000000 = 999938964.84375
assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
.isEqualTo(999938965);
}
@Test
public void toInstant_onlyAuthoritativeSourceDiscarded() {
OperationalDatasetTimestamp timestamp1 =
new OperationalDatasetTimestamp(100L, 0x7fff, false);
OperationalDatasetTimestamp timestamp2 =
OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
assertThat(timestamp2.getSeconds()).isEqualTo(100L);
assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
assertThat(timestamp2.isAuthoritativeSource()).isTrue();
}
@Test
public void constructor_tooLargeSeconds_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() ->
new OperationalDatasetTimestamp(
/* seconds= */ 0x0001112233445566L,
/* ticks= */ 0,
/* isAuthoritativeSource= */ true));
}
@Test
public void constructor_tooLargeTicks_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() ->
new OperationalDatasetTimestamp(
/* seconds= */ 0x01L,
/* ticks= */ 0x8000,
/* isAuthoritativeSource= */ true));
}
@Test
public void equalityTests() {
new EqualsTester()
.addEqualityGroup(
new OperationalDatasetTimestamp(100, 100, false),
new OperationalDatasetTimestamp(100, 100, false))
.addEqualityGroup(
new OperationalDatasetTimestamp(0, 0, false),
new OperationalDatasetTimestamp(0, 0, false))
.addEqualityGroup(
new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
.testEquals();
}
}

View File

@@ -0,0 +1,247 @@
/*
* 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 android.net.thread.cts;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.primitives.Bytes;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
/** Tests for {@link PendingOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class PendingOperationalDatasetTest {
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.createRandomDataset();
@Test
public void parcelable_parcelingIsLossLess() {
PendingOperationalDataset dataset =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
assertParcelingIsLossless(dataset);
}
@Test
public void equalityTests() {
ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset();
ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset();
new EqualsTester()
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(100)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(100)))
.testEquals();
}
@Test
public void constructor_correctValuesAreSet() {
PendingOperationalDataset dataset =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
}
@Test
public void fromThreadTlvs_openthreadTlvs_success() {
// An example Pending Operational Dataset which is generated with OpenThread CLI:
// Pending Timestamp: 2
// Active Timestamp: 1
// Channel: 26
// Channel Mask: 0x07fff800
// Delay: 46354
// Ext PAN ID: a74182f4d3f4de41
// Mesh Local Prefix: fd46:c1b9:e159:5574::/64
// Network Key: ed916e454d96fd00184f10a6f5c9e1d3
// Network Name: OpenThread-bff8
// PAN ID: 0xbff8
// PSKc: 264f78414adc683191863d968f72d1b7
// Security Policy: 672 onrc
final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
base16().lowerCase()
.decode(
"0e0800000000000100003308000000000002000034040000b51200030000"
+ "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+ "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+ "70656e5468726561642d626666380102bff80410264f78414a"
+ "dc683191863d968f72d1b70c0402a0f7f8");
PendingOperationalDataset pendingDataset =
PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
assertThat(activeDataset.getChannel()).isEqualTo(26);
assertThat(activeDataset.getChannelMask().get(0))
.isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
assertThat(activeDataset.getExtendedPanId())
.isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
assertThat(activeDataset.getMeshLocalPrefix())
.isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
assertThat(activeDataset.getNetworkKey())
.isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
assertThat(activeDataset.getPskc())
.isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(activeDataset.getSecurityPolicy().getFlags())
.isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
}
@Test
public void fromThreadTlvs_completePendingDatasetTlvs_success() {
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
// 0x34 0x04 0x0000012C (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs =
base16().decode("3308000000000001000034040000012C");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
PendingOperationalDataset dataset =
PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
}
@Test
public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() {
// Type Length Value
// 0x34 0x04 0x00000064 (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
}
@Test
public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() {
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
}
@Test
public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() {
final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs();
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
}
@Test
public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
final byte[] invalidTlvs = new byte[] {0x00};
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
}
@Test
public void toThreadTlvs_conversionIsLossLess() {
PendingOperationalDataset dataset1 =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
PendingOperationalDataset dataset2 =
PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
assertThat(dataset2).isEqualTo(dataset1);
}
}

View File

@@ -0,0 +1,50 @@
//
// 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
name: "ThreadNetworkUnitTests",
min_sdk_version: "33",
sdk_version: "module_current",
manifest: "AndroidManifest.xml",
test_config: "AndroidTest.xml",
srcs: [
"src/**/*.java",
],
test_suites: [
"general-tests",
],
static_libs: [
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"guava-android-testlib",
"net-tests-utils",
"truth-prebuilt",
],
libs: [
"android.test.base",
"android.test.runner",
],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.unittests">
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="android.net.thread.unittests"
android:label="Unit tests for android.net.thread" />
</manifest>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<configuration description="Config for Thread network unit test cases">
<option name="test-tag" value="ThreadNetworkUnitTests" />
<option name="test-suite-tag" value="apct" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
<option name="check-min-sdk" value="true" />
<option name="cleanup-apks" value="true" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.unittests" />
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
</configuration>

View File

@@ -0,0 +1,202 @@
/*
* 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 android.net.thread;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset.Builder;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.util.SparseArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.primitives.Bytes;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.security.SecureRandom;
import java.util.Random;
/** Unit tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ActiveOperationalDatasetTest {
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 19
// Channel Mask: 0x07FFF800
// Ext PAN ID: ACC214689BC40BDF
// Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
// Network Key: F26B3153760F519A63BAFDDFFC80D2AF
// Network Name: OpenThread-d9a0
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
private static final byte[] VALID_DATASET =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
@Mock private Random mockRandom;
@Mock private SecureRandom mockSecureRandom;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
private static byte[] addTlv(byte[] dataset, String tlvHex) {
return Bytes.concat(dataset, base16().decode(tlvHex));
}
@Test
public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
ActiveOperationalDataset dataset1 =
ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
ActiveOperationalDataset dataset2 =
ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
assertThat(unknownTlvs.size()).isEqualTo(2);
assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
assertThat(dataset2).isEqualTo(dataset1);
}
@Test
public void createRandomDataset_fieldsAreRandomized() {
// Always return the max bounded value
doAnswer(invocation -> (int) invocation.getArgument(0) - 1)
.when(mockRandom)
.nextInt(anyInt());
doAnswer(
invocation -> {
byte[] output = invocation.getArgument(0);
for (int i = 0; i < output.length; ++i) {
output[i] = (byte) (i + 10);
}
return null;
})
.when(mockRandom)
.nextBytes(any(byte[].class));
doAnswer(
invocation -> {
byte[] output = invocation.getArgument(0);
for (int i = 0; i < output.length; ++i) {
output[i] = (byte) (i + 30);
}
return null;
})
.when(mockSecureRandom)
.nextBytes(any(byte[].class));
ActiveOperationalDataset dataset =
ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom);
assertThat(dataset.getActiveTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
assertThat(dataset.getExtendedPanId())
.isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17});
assertThat(dataset.getMeshLocalPrefix())
.isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64"));
verify(mockRandom, times(2)).nextBytes(any(byte[].class));
assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe
verify(mockRandom, times(1)).nextInt(eq(0xffff));
assertThat(dataset.getChannel()).isEqualTo(26);
verify(mockRandom, times(1)).nextInt(eq(16));
assertThat(dataset.getChannelPage()).isEqualTo(0);
assertThat(dataset.getChannelMask().size()).isEqualTo(1);
assertThat(dataset.getPskc())
.isEqualTo(
new byte[] {
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
});
assertThat(dataset.getNetworkKey())
.isEqualTo(
new byte[] {
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
});
verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class));
assertThat(dataset.getSecurityPolicy())
.isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
}
@Test
public void builder_buildWithTooLongTlvs_throwsIllegalState() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
for (int i = 0; i < 10; i++) {
builder.addUnknownTlv(i, new byte[20]);
}
assertThrows(IllegalStateException.class, () -> new Builder().build());
}
@Test
public void builder_setUnknownTlvs_success() {
ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
unknownTlvs.put(0x33, new byte[] {1, 2, 3});
unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
ActiveOperationalDataset dataset2 =
new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
}
@Test
public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
assertThrows(
IllegalArgumentException.class,
() -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
}
@Test
public void securityPolicy_toTlvValue_conversionIsLossLess() {
SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
assertThat(policy2).isEqualTo(policy1);
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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 android.net.thread;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link OperationalDatasetTimestamp}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class OperationalDatasetTimestampTest {
@Test
public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
}
@Test
public void fromTlvValue_goodValue_success() {
OperationalDatasetTimestamp timestamp =
OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
// 0x9989 is 0x4CC4 << 1 + 1
assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
assertThat(timestamp.isAuthoritativeSource()).isTrue();
}
@Test
public void toTlvValue_conversionIsLossLess() {
OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
OperationalDatasetTimestamp timestamp2 =
OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
assertThat(timestamp2).isEqualTo(timestamp1);
}
}