summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/utils/Snappable.java35
-rw-r--r--services/core/java/com/android/server/utils/Snapshots.java133
-rw-r--r--services/core/java/com/android/server/utils/WatchableImpl.java33
-rw-r--r--services/core/java/com/android/server/utils/WatchedArrayMap.java43
-rw-r--r--services/core/java/com/android/server/utils/WatchedSparseArray.java42
-rw-r--r--services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java47
-rw-r--r--services/tests/servicestests/src/com/android/server/utils/WatchableTester.java87
-rw-r--r--services/tests/servicestests/src/com/android/server/utils/WatcherTest.java375
8 files changed, 641 insertions, 154 deletions
diff --git a/services/core/java/com/android/server/utils/Snappable.java b/services/core/java/com/android/server/utils/Snappable.java
new file mode 100644
index 000000000000..9b9460b8f757
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Snappable.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import android.annotation.NonNull;
+
+/**
+ * A class that implements Snappable can generate a read-only copy its instances. A
+ * snapshot is like a clone except that it is only required to support read-only class
+ * methods. Snapshots are immutable. Attempts to modify the state of a snapshot throw
+ * {@link UnsupporteOperationException}.
+ * @param <T> The type returned by the snapshot() method.
+ */
+public interface Snappable<T> {
+
+ /**
+ * Create an immutable copy of the object, suitable for read-only methods. A snapshot
+ * is free to omit state that is only needed for mutating methods.
+ */
+ @NonNull T snapshot();
+}
diff --git a/services/core/java/com/android/server/utils/Snapshots.java b/services/core/java/com/android/server/utils/Snapshots.java
new file mode 100644
index 000000000000..33b2bd48d802
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Snapshots.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseSetArray;
+
+/**
+ * A collection of useful methods for manipulating Snapshot classes. This is similar to
+ * java.util.Objects or java.util.Arrays.
+ */
+public class Snapshots {
+
+ /**
+ * Return the snapshot of an object, if the object extends {@link Snapper}, or the object
+ * itself.
+ * @param o The object to be copied
+ * @return A snapshot of the object, if the object extends {@link Snapper}
+ */
+ public static <T> T maybeSnapshot(T o) {
+ if (o instanceof Snappable) {
+ return ((Snappable<T>) o).snapshot();
+ } else {
+ return o;
+ }
+ }
+
+ /**
+ * Copy a SparseArray in a manner suitable for a snapshot. The destination must be
+ * empty. This is not a snapshot because the elements are copied by reference even if
+ * they are {@link Snappable}.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array
+ */
+ public <E> void copy(@NonNull SparseArray<E> dst, @NonNull SparseArray<E> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("copy destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ dst.put(src.keyAt(i), src.valueAt(i));
+ }
+ }
+
+ /**
+ * Copy a SparseSetArray in a manner suitable for a snapshot. The destination must be
+ * empty. This is not a snapshot because the elements are copied by reference even if
+ * they are {@link Snappable}.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array
+ */
+ public static <E> void copy(@NonNull SparseSetArray<E> dst, @NonNull SparseSetArray<E> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("copy destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ final int size = src.sizeAt(i);
+ for (int j = 0; j < size; j++) {
+ dst.add(src.keyAt(i), src.valueAt(i, j));
+ }
+ }
+ }
+
+ /**
+ * Make <dst> a snapshot of <src> .
+ * @param dst The destination array. It must be empty.
+ * @param src The source array
+ */
+ public void snapshot(@NonNull SparseIntArray dst, @NonNull SparseIntArray src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ dst.put(src.keyAt(i), src.valueAt(i));
+ }
+ }
+
+ /**
+ * Make <dst> a "snapshot" of <src>. <dst> mst be empty. The destination is just a
+ * copy of the source except that if the source elements implement Snappable, then
+ * the elements in the destination will be snapshots of elements from the source.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array
+ */
+ public static <E extends Snappable<E>> void snapshot(@NonNull SparseArray<E> dst,
+ @NonNull SparseArray<E> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ dst.put(src.keyAt(i), src.valueAt(i).snapshot());
+ }
+ }
+
+ /**
+ * Make <dst> a "snapshot" of <src>. <dst> mst be empty. The destination is a
+ * copy of the source except that snapshots are taken of the elements.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array
+ */
+ public static <E extends Snappable<E>> void snapshot(@NonNull SparseSetArray<E> dst,
+ @NonNull SparseSetArray<E> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ final int size = src.sizeAt(i);
+ for (int j = 0; j < size; j++) {
+ dst.add(src.keyAt(i), src.valueAt(i, j).snapshot());
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/utils/WatchableImpl.java b/services/core/java/com/android/server/utils/WatchableImpl.java
index 94ab1d49807f..16400b186ab0 100644
--- a/services/core/java/com/android/server/utils/WatchableImpl.java
+++ b/services/core/java/com/android/server/utils/WatchableImpl.java
@@ -19,11 +19,15 @@ package com.android.server.utils;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import com.android.internal.annotations.GuardedBy;
+
import java.util.ArrayList;
import java.util.Objects;
/**
- * A concrete implementation of {@link Watchable}
+ * A concrete implementation of {@link Watchable}. This includes one commonly needed feature:
+ * the Watchable may be sealed, so that it throws an {@link IllegalStateException} if
+ * a change is detected.
*/
public class WatchableImpl implements Watchable {
/**
@@ -78,10 +82,37 @@ public class WatchableImpl implements Watchable {
@Override
public void dispatchChange(@Nullable Watchable what) {
synchronized (mObservers) {
+ if (mSealed) {
+ throw new IllegalStateException("attempt to change a sealed object");
+ }
final int end = mObservers.size();
for (int i = 0; i < end; i++) {
mObservers.get(i).onChange(what);
}
}
}
+
+ /**
+ * True if the object is sealed.
+ */
+ @GuardedBy("mObservers")
+ private boolean mSealed = false;
+
+ /**
+ * Freeze the {@link Watchable}.
+ **/
+ public void seal() {
+ synchronized (mObservers) {
+ mSealed = true;
+ }
+ }
+
+ /**
+ * Return the sealed state.
+ */
+ public boolean isFrozen() {
+ synchronized (mObservers) {
+ return mSealed;
+ }
+ }
}
diff --git a/services/core/java/com/android/server/utils/WatchedArrayMap.java b/services/core/java/com/android/server/utils/WatchedArrayMap.java
index 7b3298086aba..e8065f140af7 100644
--- a/services/core/java/com/android/server/utils/WatchedArrayMap.java
+++ b/services/core/java/com/android/server/utils/WatchedArrayMap.java
@@ -18,9 +18,7 @@ package com.android.server.utils;
import android.annotation.NonNull;
import android.annotation.Nullable;
-
import android.util.ArrayMap;
-import android.util.Log;
import java.util.Collection;
import java.util.Collections;
@@ -31,14 +29,17 @@ import java.util.Set;
* WatchedArrayMap is an {@link android.util.ArrayMap} that can report changes to itself. If its
* values are {@link Watchable} then the WatchedArrayMap will also report changes to the values.
* A {@link Watchable} is notified only once, no matter how many times it is stored in the array.
+ * @param <K> The key type.
+ * @param <V> The value type.
*/
-public class WatchedArrayMap<K, V> extends WatchableImpl implements Map<K, V> {
+public class WatchedArrayMap<K, V> extends WatchableImpl
+ implements Map<K, V>, Snappable {
// The storage
private final ArrayMap<K, V> mStorage;
// If true, the array is watching its children
- private boolean mWatching = false;
+ private volatile boolean mWatching = false;
// The local observer
private final Watcher mObserver = new Watcher() {
@@ -386,4 +387,38 @@ public class WatchedArrayMap<K, V> extends WatchableImpl implements Map<K, V> {
onChanged();
return result;
}
+
+ /**
+ * Create a copy of the array. If the element is a subclass of Snapper then the copy
+ * contains snapshots of the elements. Otherwise the copy contains references to the
+ * elements. The returned snapshot is immutable.
+ * @return A new array whose elements are the elements of <this>.
+ */
+ public WatchedArrayMap<K, V> snapshot() {
+ WatchedArrayMap<K, V> l = new WatchedArrayMap<>();
+ snapshot(l, this);
+ return l;
+ }
+
+ /**
+ * Make the destination a copy of the source. If the element is a subclass of Snapper then the
+ * copy contains snapshots of the elements. Otherwise the copy contains references to the
+ * elements. The destination must be initially empty. Upon return, the destination is
+ * immutable.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array. It is not modified.
+ */
+ public static <K, V> void snapshot(@NonNull WatchedArrayMap<K, V> dst,
+ @NonNull WatchedArrayMap<K, V> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ final V val = Snapshots.maybeSnapshot(src.valueAt(i));
+ final K key = src.keyAt(i);
+ dst.put(key, val);
+ }
+ dst.seal();
+ }
}
diff --git a/services/core/java/com/android/server/utils/WatchedSparseArray.java b/services/core/java/com/android/server/utils/WatchedSparseArray.java
index 4d43b682e113..6797c6eb7801 100644
--- a/services/core/java/com/android/server/utils/WatchedSparseArray.java
+++ b/services/core/java/com/android/server/utils/WatchedSparseArray.java
@@ -18,7 +18,6 @@ package com.android.server.utils;
import android.annotation.NonNull;
import android.annotation.Nullable;
-
import android.util.SparseArray;
import java.util.ArrayList;
@@ -28,14 +27,16 @@ import java.util.ArrayList;
* array registers with the {@link Watchable}. The array registers only once with each
* {@link Watchable} no matter how many times the {@link Watchable} is stored in the
* array.
+ * @param <E> The element type, stored in the array.
*/
-public class WatchedSparseArray<E> extends WatchableImpl {
+public class WatchedSparseArray<E> extends WatchableImpl
+ implements Snappable {
// The storage
private final SparseArray<E> mStorage;
// If true, the array is watching its children
- private boolean mWatching = false;
+ private volatile boolean mWatching = false;
// The local observer
private final Watcher mObserver = new Watcher() {
@@ -398,4 +399,39 @@ public class WatchedSparseArray<E> extends WatchableImpl {
public String toString() {
return mStorage.toString();
}
+
+ /**
+ * Create a copy of the array. If the element is a subclass of Snapper then the copy
+ * contains snapshots of the elements. Otherwise the copy contains references to the
+ * elements. The returned snapshot is immutable.
+ * @return A new array whose elements are the elements of <this>.
+ */
+ public WatchedSparseArray<E> snapshot() {
+ WatchedSparseArray<E> l = new WatchedSparseArray<>();
+ snapshot(l, this);
+ return l;
+ }
+
+ /**
+ * Make the destination a copy of the source. If the element is a subclass of Snapper then the
+ * copy contains snapshots of the elements. Otherwise the copy contains references to the
+ * elements. The destination must be initially empty. Upon return, the destination is
+ * immutable.
+ * @param dst The destination array. It must be empty.
+ * @param src The source array. It is not modified.
+ */
+ public static <E> void snapshot(@NonNull WatchedSparseArray<E> dst,
+ @NonNull WatchedSparseArray<E> src) {
+ if (dst.size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = src.size();
+ for (int i = 0; i < end; i++) {
+ final E val = Snapshots.maybeSnapshot(src.valueAt(i));
+ final int key = src.keyAt(i);
+ dst.put(key, val);
+ }
+ dst.seal();
+ }
+
}
diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java
index edf7d27b61dd..b845eea168a5 100644
--- a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java
+++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java
@@ -17,21 +17,20 @@
package com.android.server.utils;
import android.annotation.NonNull;
-import android.annotation.Nullable;
-
import android.util.SparseBooleanArray;
/**
* A watched variant of SparseBooleanArray. Changes to the array are notified to
* registered {@link Watcher}s.
*/
-public class WatchedSparseBooleanArray extends WatchableImpl {
+public class WatchedSparseBooleanArray extends WatchableImpl
+ implements Snappable {
// The storage
private final SparseBooleanArray mStorage;
// A private convenience function
- private void dispatchChange() {
+ private void onChanged() {
dispatchChange(this);
}
@@ -81,7 +80,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
*/
public void delete(int key) {
mStorage.delete(key);
- dispatchChange();
+ onChanged();
}
/**
@@ -91,7 +90,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
*/
public void removeAt(int index) {
mStorage.removeAt(index);
- dispatchChange();
+ onChanged();
}
/**
@@ -102,7 +101,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
public void put(int key, boolean value) {
if (mStorage.get(key) != value) {
mStorage.put(key, value);
- dispatchChange();
+ onChanged();
}
}
@@ -164,7 +163,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
public void setValueAt(int index, boolean value) {
if (mStorage.valueAt(index) != value) {
mStorage.setValueAt(index, value);
- dispatchChange();
+ onChanged();
}
}
@@ -172,7 +171,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
public void setKeyAt(int index, int key) {
if (mStorage.keyAt(index) != key) {
mStorage.setKeyAt(index, key);
- dispatchChange();
+ onChanged();
}
}
@@ -202,7 +201,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
*/
public void clear() {
mStorage.clear();
- dispatchChange();
+ onChanged();
}
/**
@@ -211,7 +210,7 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
*/
public void append(int key, boolean value) {
mStorage.append(key, value);
- dispatchChange();
+ onChanged();
}
@Override
@@ -233,4 +232,30 @@ public class WatchedSparseBooleanArray extends WatchableImpl {
public String toString() {
return mStorage.toString();
}
+
+ /**
+ * Create a snapshot. The snapshot does not include any {@link Watchable}
+ * information.
+ */
+ public WatchedSparseBooleanArray snapshot() {
+ WatchedSparseBooleanArray l = new WatchedSparseBooleanArray(this);
+ l.seal();
+ return l;
+ }
+
+ /**
+ * Make <this> a snapshot of the argument. Note that <this> is immutable when the
+ * method returns. <this> must be empty when the function is called.
+ * @param r The source array, which is copied into <this>
+ */
+ public void snapshot(@NonNull WatchedSparseBooleanArray r) {
+ if (size() != 0) {
+ throw new IllegalArgumentException("snapshot destination is not empty");
+ }
+ final int end = r.size();
+ for (int i = 0; i < end; i++) {
+ put(r.keyAt(i), r.valueAt(i));
+ }
+ seal();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/utils/WatchableTester.java b/services/tests/servicestests/src/com/android/server/utils/WatchableTester.java
new file mode 100644
index 000000000000..590df3c18f5a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/utils/WatchableTester.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * A class to count the number of notifications received.
+ */
+public class WatchableTester extends Watcher {
+
+ // The count of changes.
+ public int mChanges = 0;
+
+ // The change count at the last verifyChangeReported() call.
+ public int mLastChangeCount = 0;
+
+ // The single Watchable that this monitors.
+ public final Watchable mWatched;
+
+ // The key, used for messages
+ public String mKey;
+
+ // Clear the changes count, for when the tester is reused.
+ public void clear() {
+ mChanges = 0;
+ }
+
+ /**
+ * Create the WatchableTester with a Watcher and a key. The key is used for logging
+ * test failures.
+ * @param w The {@link Watchable} under test
+ * @param k A key that is prefixed to any test failures.
+ **/
+ public WatchableTester(Watchable w, String k) {
+ mWatched = w;
+ mKey = k;
+ }
+
+ // Listen for events
+ public void register() {
+ mWatched.registerObserver(this);
+ }
+
+ // Stop listening for events
+ public void unregister() {
+ mWatched.unregisterObserver(this);
+ }
+
+ // Count the number of notifications received.
+ @Override
+ public void onChange(Watchable what) {
+ mChanges++;
+ }
+
+ // Verify the count.
+ public void verify(int want, String msg) {
+ assertEquals(mKey + " " + msg, want, mChanges);
+ }
+
+ // Verify that at least one change was reported since the last verify. The actual
+ // number of changes is not important. This resets the count of changes.
+ public void verifyChangeReported(String msg) {
+ assertTrue(mKey + " " + msg, mLastChangeCount < mChanges);
+ mLastChangeCount = mChanges;
+ }
+
+ // Verify that no change was reported since the last verify.
+ public void verifyNoChangeReported(String msg) {
+ assertTrue(mKey + " " + msg, mLastChangeCount == mChanges);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
index 40575e4cf16f..9bea9d4cedbd 100644
--- a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
@@ -18,15 +18,10 @@ package com.android.server.utils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
+import static org.junit.Assert.fail;
import androidx.test.filters.SmallTest;
-import com.android.internal.util.Preconditions;
-import com.android.internal.util.TraceBuffer;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -42,56 +37,76 @@ import org.junit.Test;
@SmallTest
public class WatcherTest {
+ // A counter to generate unique IDs for Leaf elements.
+ private int mLeafId = 0;
+
+ // Useful indices used int the tests.
+ private static final int INDEX_A = 1;
+ private static final int INDEX_B = 2;
+ private static final int INDEX_C = 3;
+ private static final int INDEX_D = 4;
+
// A small Watchable leaf node
- private class Leaf extends WatchableImpl {
- private int datum = 0;
+ private class Leaf extends WatchableImpl implements Snappable {
+ private int mId;
+ private int mDatum;
+
+ Leaf() {
+ mDatum = 0;
+ mId = mLeafId++;
+ }
+
void set(int i) {
- if (datum != i) {
- datum = i;
+ if (mDatum != i) {
+ mDatum = i;
dispatchChange(this);
}
}
+ int get() {
+ return mDatum;
+ }
void tick() {
- set(datum + 1);
+ set(mDatum + 1);
}
- }
-
- // A top-most watcher. It counts the number of notifications that it receives.
- private class Tester extends Watcher {
- // The count of changes.
- public int changes = 0;
-
- // The single Watchable that this monitors.
- public final Watchable mWatched;
-
- // The key, used for messages
- public String mKey;
-
- // Create the Tester with a Watcher
- public Tester(Watchable w, String k) {
- mWatched = w;
- mKey = k;
+ public Leaf snapshot() {
+ Leaf result = new Leaf();
+ result.mDatum = mDatum;
+ result.mId = mId;
+ result.seal();
+ return result;
}
-
- // Listen for events
- public void register() {
- mWatched.registerObserver(this);
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Leaf) {
+ return mDatum == ((Leaf) o).mDatum && mId == ((Leaf) o).mId;
+ } else {
+ return false;
+ }
}
-
- // Stop listening for events
- public void unregister() {
- mWatched.unregisterObserver(this);
+ @Override
+ public String toString() {
+ return "Leaf(" + mDatum + "," + mId + ")";
}
+ }
- // Count the number of notifications received.
- @Override
- public void onChange(Watchable what) {
- changes++;
+ // Execute the {@link Runnable} and if {@link UnsupportedOperationException} is
+ // thrown, do nothing. If no exception is thrown, fail the test.
+ private void verifySealed(String msg, Runnable test) {
+ try {
+ test.run();
+ fail(msg + " should be sealed");
+ } catch (IllegalStateException e) {
+ // The exception was expected.
}
+ }
- // Verify the count.
- public void verify(int want, String msg) {
- assertEquals(mKey + " " + msg, want, changes);
+ // Execute the {@link Runnable} and if {@link UnsupportedOperationException} is
+ // thrown, fail the test. If no exception is thrown, do nothing.
+ private void verifyNotSealed(String msg, Runnable test) {
+ try {
+ test.run();
+ } catch (IllegalStateException e) {
+ fail(msg + " should be not sealed");
}
}
@@ -104,168 +119,212 @@ public class WatcherTest {
}
@Test
- public void test_notify() {
-
- Tester tester;
+ public void testBasicBehavior() {
+ WatchableTester tester;
// Create a few leaves
- Leaf a = new Leaf();
- Leaf b = new Leaf();
- Leaf c = new Leaf();
- Leaf d = new Leaf();
+ Leaf leafA = new Leaf();
// Basic test. Create a leaf and verify that changes to the leaf get notified to
// the tester.
- tester = new Tester(a, "Leaf");
+ tester = new WatchableTester(leafA, "Leaf");
tester.verify(0, "Initial leaf - no registration");
- a.tick();
+ leafA.tick();
tester.verify(0, "Updates with no registration");
tester.register();
- a.tick();
+ leafA.tick();
tester.verify(1, "Updates with registration");
- a.tick();
- a.tick();
+ leafA.tick();
+ leafA.tick();
tester.verify(3, "Updates with registration");
+ // Create a snapshot. Verify that the snapshot matches the
+ Leaf leafASnapshot = leafA.snapshot();
+ assertEquals("Leaf snapshot", leafA.get(), leafASnapshot.get());
+ leafA.tick();
+ assertTrue(leafA.get() != leafASnapshot.get());
+ tester.verify(4, "Tick after snapshot");
+ verifySealed("Leaf", ()->leafASnapshot.tick());
// Add the same leaf to more than one tester. Verify that a change to the leaf is seen by
// all registered listeners.
- Tester buddy1 = new Tester(a, "Leaf2");
- Tester buddy2 = new Tester(a, "Leaf3");
+ tester.clear();
+ WatchableTester buddy1 = new WatchableTester(leafA, "Leaf2");
+ WatchableTester buddy2 = new WatchableTester(leafA, "Leaf3");
buddy1.verify(0, "Initial leaf - no registration");
buddy2.verify(0, "Initial leaf - no registration");
- a.tick();
- tester.verify(4, "Updates with buddies");
+ leafA.tick();
+ tester.verify(1, "Updates with buddies");
buddy1.verify(0, "Updates - no registration");
buddy2.verify(0, "Updates - no registration");
buddy1.register();
buddy2.register();
buddy1.verify(0, "No updates - registered");
buddy2.verify(0, "No updates - registered");
- a.tick();
+ leafA.tick();
buddy1.verify(1, "First update");
buddy2.verify(1, "First update");
buddy1.unregister();
- a.tick();
+ leafA.tick();
buddy1.verify(1, "Second update - unregistered");
buddy2.verify(2, "Second update");
+ }
- buddy1 = null;
- buddy2 = null;
+ @Test
+ public void testWatchedArrayMap() {
+ WatchableTester tester;
- final int INDEX_A = 1;
- final int INDEX_B = 2;
- final int INDEX_C = 3;
- final int INDEX_D = 4;
+ // Create a few leaves
+ Leaf leafA = new Leaf();
+ Leaf leafB = new Leaf();
+ Leaf leafC = new Leaf();
+ Leaf leafD = new Leaf();
// Test WatchedArrayMap
- WatchedArrayMap<Integer, Leaf> am = new WatchedArrayMap<>();
- am.put(INDEX_A, a);
- am.put(INDEX_B, b);
- tester = new Tester(am, "WatchedArrayMap");
+ WatchedArrayMap<Integer, Leaf> array = new WatchedArrayMap<>();
+ array.put(INDEX_A, leafA);
+ array.put(INDEX_B, leafB);
+ tester = new WatchableTester(array, "WatchedArrayMap");
tester.verify(0, "Initial array - no registration");
- a.tick();
+ leafA.tick();
tester.verify(0, "Updates with no registration");
tester.register();
tester.verify(0, "Updates with no registration");
- a.tick();
+ leafA.tick();
tester.verify(1, "Updates with registration");
- b.tick();
+ leafB.tick();
tester.verify(2, "Updates with registration");
- am.remove(INDEX_B);
+ array.remove(INDEX_B);
tester.verify(3, "Removed b");
- b.tick();
+ leafB.tick();
tester.verify(3, "Updates with b not watched");
- am.put(INDEX_B, b);
- am.put(INDEX_C, b);
+ array.put(INDEX_B, leafB);
+ array.put(INDEX_C, leafB);
tester.verify(5, "Added b twice");
- b.tick();
+ leafB.tick();
tester.verify(6, "Changed b - single notification");
- am.remove(INDEX_C);
+ array.remove(INDEX_C);
tester.verify(7, "Removed first b");
- b.tick();
+ leafB.tick();
tester.verify(8, "Changed b - single notification");
- am.remove(INDEX_B);
+ array.remove(INDEX_B);
tester.verify(9, "Removed second b");
- b.tick();
+ leafB.tick();
tester.verify(9, "Updated b - no change");
- am.clear();
+ array.clear();
tester.verify(10, "Cleared array");
- b.tick();
+ leafB.tick();
tester.verify(10, "Change to b not in array");
// Special methods
- am.put(INDEX_C, c);
+ array.put(INDEX_C, leafC);
tester.verify(11, "Added c");
- c.tick();
+ leafC.tick();
tester.verify(12, "Ticked c");
- am.setValueAt(am.indexOfKey(INDEX_C), d);
+ array.setValueAt(array.indexOfKey(INDEX_C), leafD);
tester.verify(13, "Replaced c with d");
- c.tick();
- d.tick();
+ leafC.tick();
+ leafD.tick();
tester.verify(14, "Ticked d and c (c not registered)");
- am = null;
+ // Snapshot
+ {
+ final WatchedArrayMap<Integer, Leaf> arraySnap = array.snapshot();
+ tester.verify(14, "Generate snapshot (no changes)");
+ // Verify that the snapshot is a proper copy of the source.
+ assertEquals("WatchedArrayMap snap same size",
+ array.size(), arraySnap.size());
+ for (int i = 0; i < array.size(); i++) {
+ for (int j = 0; j < arraySnap.size(); j++) {
+ assertTrue("WatchedArrayMap elements differ",
+ array.valueAt(i) != arraySnap.valueAt(j));
+ }
+ assertTrue("WatchedArrayMap element copy",
+ array.valueAt(i).equals(arraySnap.valueAt(i)));
+ }
+ leafD.tick();
+ tester.verify(15, "Tick after snapshot");
+ // Verify that the snapshot is sealed
+ verifySealed("WatchedArrayMap", ()->arraySnap.put(INDEX_A, leafA));
+ }
+ // Recreate the snapshot since the test corrupted it.
+ {
+ final WatchedArrayMap<Integer, Leaf> arraySnap = array.snapshot();
+ // Verify that elements are also snapshots
+ final Leaf arraySnapElement = arraySnap.valueAt(0);
+ verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick());
+ }
+ }
+
+ @Test
+ public void testWatchedSparseArray() {
+ WatchableTester tester;
+
+ // Create a few leaves
+ Leaf leafA = new Leaf();
+ Leaf leafB = new Leaf();
+ Leaf leafC = new Leaf();
+ Leaf leafD = new Leaf();
// Test WatchedSparseArray
- WatchedSparseArray<Leaf> sa = new WatchedSparseArray<>();
- sa.put(INDEX_A, a);
- sa.put(INDEX_B, b);
- tester = new Tester(sa, "WatchedSparseArray");
+ WatchedSparseArray<Leaf> array = new WatchedSparseArray<>();
+ array.put(INDEX_A, leafA);
+ array.put(INDEX_B, leafB);
+ tester = new WatchableTester(array, "WatchedSparseArray");
tester.verify(0, "Initial array - no registration");
- a.tick();
+ leafA.tick();
tester.verify(0, "Updates with no registration");
tester.register();
tester.verify(0, "Updates with no registration");
- a.tick();
+ leafA.tick();
tester.verify(1, "Updates with registration");
- b.tick();
+ leafB.tick();
tester.verify(2, "Updates with registration");
- sa.remove(INDEX_B);
+ array.remove(INDEX_B);
tester.verify(3, "Removed b");
- b.tick();
+ leafB.tick();
tester.verify(3, "Updates with b not watched");
- sa.put(INDEX_B, b);
- sa.put(INDEX_C, b);
+ array.put(INDEX_B, leafB);
+ array.put(INDEX_C, leafB);
tester.verify(5, "Added b twice");
- b.tick();
+ leafB.tick();
tester.verify(6, "Changed b - single notification");
- sa.remove(INDEX_C);
+ array.remove(INDEX_C);
tester.verify(7, "Removed first b");
- b.tick();
+ leafB.tick();
tester.verify(8, "Changed b - single notification");
- sa.remove(INDEX_B);
+ array.remove(INDEX_B);
tester.verify(9, "Removed second b");
- b.tick();
- tester.verify(9, "Updated b - no change");
- sa.clear();
+ leafB.tick();
+ tester.verify(9, "Updated leafB - no change");
+ array.clear();
tester.verify(10, "Cleared array");
- b.tick();
+ leafB.tick();
tester.verify(10, "Change to b not in array");
// Special methods
- sa.put(INDEX_A, a);
- sa.put(INDEX_B, b);
- sa.put(INDEX_C, c);
+ array.put(INDEX_A, leafA);
+ array.put(INDEX_B, leafB);
+ array.put(INDEX_C, leafC);
tester.verify(13, "Added c");
- c.tick();
+ leafC.tick();
tester.verify(14, "Ticked c");
- sa.setValueAt(sa.indexOfKey(INDEX_C), d);
+ array.setValueAt(array.indexOfKey(INDEX_C), leafD);
tester.verify(15, "Replaced c with d");
- c.tick();
- d.tick();
+ leafC.tick();
+ leafD.tick();
tester.verify(16, "Ticked d and c (c not registered)");
- sa.append(INDEX_D, c);
+ array.append(INDEX_D, leafC);
tester.verify(17, "Append c");
- c.tick();
- d.tick();
+ leafC.tick();
+ leafD.tick();
tester.verify(19, "Ticked d and c");
- assertEquals("Verify four elements", 4, sa.size());
+ assertEquals("Verify four elements", 4, array.size());
// Figure out which elements are at which indices.
Leaf[] x = new Leaf[4];
for (int i = 0; i < 4; i++) {
- x[i] = sa.valueAt(i);
+ x[i] = array.valueAt(i);
}
- sa.removeAtRange(0, 2);
+ array.removeAtRange(0, 2);
tester.verify(20, "Removed two elements in one operation");
x[0].tick();
x[1].tick();
@@ -274,31 +333,77 @@ public class WatcherTest {
x[3].tick();
tester.verify(22, "Ticked two remaining elements");
- sa = null;
+ // Snapshot
+ {
+ final WatchedSparseArray<Leaf> arraySnap = array.snapshot();
+ tester.verify(22, "Generate snapshot (no changes)");
+ // Verify that the snapshot is a proper copy of the source.
+ assertEquals("WatchedSparseArray snap same size",
+ array.size(), arraySnap.size());
+ for (int i = 0; i < array.size(); i++) {
+ for (int j = 0; j < arraySnap.size(); j++) {
+ assertTrue("WatchedSparseArray elements differ",
+ array.valueAt(i) != arraySnap.valueAt(j));
+ }
+ assertTrue("WatchedArrayMap element copy",
+ array.valueAt(i).equals(arraySnap.valueAt(i)));
+ }
+ leafD.tick();
+ tester.verify(23, "Tick after snapshot");
+ // Verify that the array snapshot is sealed
+ verifySealed("WatchedSparseArray", ()->arraySnap.put(INDEX_A, leafB));
+ }
+ // Recreate the snapshot since the test corrupted it.
+ {
+ final WatchedSparseArray<Leaf> arraySnap = array.snapshot();
+ // Verify that elements are also snapshots
+ final Leaf arraySnapElement = arraySnap.valueAt(0);
+ verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick());
+ }
+ }
+
+ @Test
+ public void testWatchedSparseBooleanArray() {
+ WatchableTester tester;
// Test WatchedSparseBooleanArray
- WatchedSparseBooleanArray sb = new WatchedSparseBooleanArray();
- tester = new Tester(sb, "WatchedSparseBooleanArray");
+ WatchedSparseBooleanArray array = new WatchedSparseBooleanArray();
+ tester = new WatchableTester(array, "WatchedSparseBooleanArray");
tester.verify(0, "Initial array - no registration");
- sb.put(INDEX_A, true);
+ array.put(INDEX_A, true);
tester.verify(0, "Updates with no registration");
tester.register();
tester.verify(0, "Updates with no registration");
- sb.put(INDEX_B, true);
+ array.put(INDEX_B, true);
tester.verify(1, "Updates with registration");
- sb.put(INDEX_B, true);
+ array.put(INDEX_B, true);
tester.verify(1, "Null update");
- sb.put(INDEX_B, false);
- sb.put(INDEX_C, true);
+ array.put(INDEX_B, false);
+ array.put(INDEX_C, true);
tester.verify(3, "Updates with registration");
// Special methods
- sb.put(INDEX_C, true);
+ array.put(INDEX_C, true);
tester.verify(3, "Added true, no change");
- sb.setValueAt(sb.indexOfKey(INDEX_C), false);
+ array.setValueAt(array.indexOfKey(INDEX_C), false);
tester.verify(4, "Replaced true with false");
- sb.append(INDEX_D, true);
+ array.append(INDEX_D, true);
tester.verify(5, "Append true");
- sb = null;
+ // Snapshot
+ {
+ WatchedSparseBooleanArray arraySnap = array.snapshot();
+ tester.verify(5, "Generate snapshot");
+ // Verify that the snapshot is a proper copy of the source.
+ assertEquals("WatchedSparseBooleanArray snap same size",
+ array.size(), arraySnap.size());
+ for (int i = 0; i < array.size(); i++) {
+ assertEquals("WatchedSparseArray element copy",
+ array.valueAt(i), arraySnap.valueAt(i));
+ }
+ array.put(INDEX_D, false);
+ tester.verify(6, "Tick after snapshot");
+ // Verify that the array is sealed
+ verifySealed("WatchedSparseBooleanArray", ()->arraySnap.put(INDEX_D, false));
+ }
}
}