Merge "PackageManager lock reduction utilities"

This commit is contained in:
TreeHugger Robot
2020-12-02 17:30:45 +00:00
committed by Android (Google) Code Review
8 changed files with 643 additions and 156 deletions

View File

@@ -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();
}

View File

@@ -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());
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
public Leaf snapshot() {
Leaf result = new Leaf();
result.mDatum = mDatum;
result.mId = mId;
result.seal();
return result;
}
@Override
public boolean equals(Object o) {
if (o instanceof Leaf) {
return mDatum == ((Leaf) o).mDatum && mId == ((Leaf) o).mId;
} else {
return false;
}
}
@Override
public String toString() {
return "Leaf(" + mDatum + "," + mId + ")";
}
}
// 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;
// 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.
}
}
// 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) {
changes++;
}
// 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));
}
}
}