diff options
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)); + } } } |