PackageManager lock reduction utilities
Bug: 161323622 Define a Snapshotable interface that marks a class as supporting the snapshot() methods. A snapshot is an immutable copy of a class that supports read-only methods. Class attributes that are not required for read-only methods are not coiped into the snapshot. Watchable classes that are are Snapshotable exploit the Watchable machinery to throw an exception if code attempts to modify a snapshot. The Snapshotable functionality is tested in the existing WatcherTest unit test. Test: atest * FrameworksServicesTests:WatcherTest Change-Id: I279b378ddf71c3e753fcc4776e22fe859123bc14
This commit is contained in:
35
services/core/java/com/android/server/utils/Snappable.java
Normal file
35
services/core/java/com/android/server/utils/Snappable.java
Normal 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();
|
||||
}
|
||||
133
services/core/java/com/android/server/utils/Snapshots.java
Normal file
133
services/core/java/com/android/server/utils/Snapshots.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user