Merge "Extract an "ArrayMapWithHistory" support class"

am: 472f9b1540

Change-Id: Ie6d8ce05f008796463cfc0e636b8ea7846166f62
This commit is contained in:
Neil Fuller
2019-12-19 14:54:19 -08:00
committed by android-build-merger
6 changed files with 648 additions and 92 deletions

View File

@@ -24,7 +24,6 @@ import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.LocalLog;
import android.util.Slog;
import android.util.TimestampedValue;
@@ -32,12 +31,11 @@ import android.util.TimestampedValue;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.timezonedetector.ArrayMapWithHistory;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedList;
import java.util.Map;
/**
* An implementation of TimeDetectorStrategy that passes phone and manual suggestions to
@@ -99,14 +97,12 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
private TimestampedValue<Long> mLastAutoSystemClockTimeSet;
/**
* A mapping from phoneId to a linked list of time suggestions (the "first" being the latest).
* We typically expect one or two entries in this Map: devices will have a small number
* of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
* the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size.
* A mapping from phoneId to a time suggestion. We typically expect one or two mappings: devices
* will have a small number of telephony devices and phoneIds are assumed to be stable.
*/
@GuardedBy("this")
private ArrayMap<Integer, LinkedList<PhoneTimeSuggestion>> mSuggestionByPhoneId =
new ArrayMap<>();
private ArrayMapWithHistory<Integer, PhoneTimeSuggestion> mSuggestionByPhoneId =
new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
@Override
public void initialize(@NonNull Callback callback) {
@@ -179,16 +175,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
ipw.println("Phone suggestion history:");
ipw.increaseIndent(); // level 2
for (Map.Entry<Integer, LinkedList<PhoneTimeSuggestion>> entry
: mSuggestionByPhoneId.entrySet()) {
ipw.println("Phone " + entry.getKey());
ipw.increaseIndent(); // level 3
for (PhoneTimeSuggestion suggestion : entry.getValue()) {
ipw.println(suggestion);
}
ipw.decreaseIndent(); // level 3
}
mSuggestionByPhoneId.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
@@ -205,20 +192,10 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
int phoneId = suggestion.getPhoneId();
LinkedList<PhoneTimeSuggestion> phoneSuggestions = mSuggestionByPhoneId.get(phoneId);
if (phoneSuggestions == null) {
// The first time we've seen this phoneId.
phoneSuggestions = new LinkedList<>();
mSuggestionByPhoneId.put(phoneId, phoneSuggestions);
} else if (phoneSuggestions.isEmpty()) {
Slog.w(LOG_TAG, "Suggestions unexpectedly empty when adding suggestion=" + suggestion);
}
if (!phoneSuggestions.isEmpty()) {
PhoneTimeSuggestion previousSuggestion = mSuggestionByPhoneId.get(phoneId);
if (previousSuggestion != null) {
// We can log / discard suggestions with obvious issues with the reference time clock.
PhoneTimeSuggestion previousSuggestion = phoneSuggestions.getFirst();
if (previousSuggestion == null
|| previousSuggestion.getUtcTime() == null
if (previousSuggestion.getUtcTime() == null
|| previousSuggestion.getUtcTime().getValue() == null) {
// This should be impossible given we only store validated suggestions.
Slog.w(LOG_TAG, "Previous suggestion is null or has a null time."
@@ -240,10 +217,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
// Store the latest suggestion.
phoneSuggestions.addFirst(suggestion);
if (phoneSuggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) {
phoneSuggestions.removeLast();
}
mSuggestionByPhoneId.put(phoneId, suggestion);
return true;
}
@@ -331,15 +305,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
int bestScore = PHONE_INVALID_SCORE;
for (int i = 0; i < mSuggestionByPhoneId.size(); i++) {
Integer phoneId = mSuggestionByPhoneId.keyAt(i);
LinkedList<PhoneTimeSuggestion> phoneSuggestions = mSuggestionByPhoneId.valueAt(i);
if (phoneSuggestions == null) {
// Unexpected - map is missing a value.
Slog.w(LOG_TAG, "Suggestions unexpectedly missing for phoneId."
+ " phoneId=" + phoneId);
continue;
}
PhoneTimeSuggestion candidateSuggestion = phoneSuggestions.getFirst();
PhoneTimeSuggestion candidateSuggestion = mSuggestionByPhoneId.valueAt(i);
if (candidateSuggestion == null) {
// Unexpected - null suggestions should never be stored.
Slog.w(LOG_TAG, "Latest suggestion unexpectedly null for phoneId."
@@ -540,10 +506,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
@VisibleForTesting
@Nullable
public synchronized PhoneTimeSuggestion getLatestPhoneSuggestion(int phoneId) {
LinkedList<PhoneTimeSuggestion> suggestions = mSuggestionByPhoneId.get(phoneId);
if (suggestions == null) {
return null;
}
return suggestions.getFirst();
return mSuggestionByPhoneId.get(phoneId);
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2019 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.timezonedetector;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
/**
* A partial decorator for {@link ArrayMap} that records historic values for each mapping for
* debugging later with {@link #dump(IndentingPrintWriter)}.
*
* <p>This class is only intended for use in {@link TimeZoneDetectorStrategy} and
* {@link com.android.server.timedetector.TimeDetectorStrategy} so only provides the parts of the
* {@link ArrayMap} API needed. If it is ever extended to include deletion methods like
* {@link ArrayMap#remove(Object)} some thought would need to be given to the correct
* {@link ArrayMap#containsKey(Object)} behavior for the history. Like {@link ArrayMap}, it is not
* thread-safe.
*
* @param <K> the type of the key
* @param <V> the type of the value
*/
public final class ArrayMapWithHistory<K, V> {
private static final String TAG = "ArrayMapWithHistory";
/** The size the linked list against each value is allowed to grow to. */
private final int mMaxHistorySize;
@Nullable
private ArrayMap<K, ReferenceWithHistory<V>> mMap;
/**
* Creates an instance that records, at most, the specified number of values against each key.
*/
public ArrayMapWithHistory(@IntRange(from = 1) int maxHistorySize) {
if (maxHistorySize < 1) {
throw new IllegalArgumentException("maxHistorySize < 1: " + maxHistorySize);
}
mMaxHistorySize = maxHistorySize;
}
/**
* See {@link ArrayMap#put(K, V)}.
*/
@Nullable
public V put(@Nullable K key, @Nullable V value) {
if (mMap == null) {
mMap = new ArrayMap<>();
}
ReferenceWithHistory<V> valueHolder = mMap.get(key);
if (valueHolder == null) {
valueHolder = new ReferenceWithHistory<>(mMaxHistorySize);
mMap.put(key, valueHolder);
} else if (valueHolder.getHistoryCount() == 0) {
Log.w(TAG, "History for \"" + key + "\" was unexpectedly empty");
}
return valueHolder.set(value);
}
/**
* See {@link ArrayMap#get(Object)}.
*/
@Nullable
public V get(@Nullable Object key) {
if (mMap == null) {
return null;
}
ReferenceWithHistory<V> valueHolder = mMap.get(key);
if (valueHolder == null) {
return null;
} else if (valueHolder.getHistoryCount() == 0) {
Log.w(TAG, "History for \"" + key + "\" was unexpectedly empty");
}
return valueHolder.get();
}
/**
* See {@link ArrayMap#size()}.
*/
public int size() {
return mMap == null ? 0 : mMap.size();
}
/**
* See {@link ArrayMap#keyAt(int)}.
*/
@Nullable
public K keyAt(int index) {
if (mMap == null) {
throw new ArrayIndexOutOfBoundsException(index);
}
return mMap.keyAt(index);
}
/**
* See {@link ArrayMap#valueAt(int)}.
*/
@Nullable
public V valueAt(int index) {
if (mMap == null) {
throw new ArrayIndexOutOfBoundsException(index);
}
ReferenceWithHistory<V> valueHolder = mMap.valueAt(index);
if (valueHolder == null || valueHolder.getHistoryCount() == 0) {
Log.w(TAG, "valueAt(" + index + ") was unexpectedly null or empty");
return null;
}
return valueHolder.get();
}
/**
* Dumps the content of the map, including historic values, using the supplied writer.
*/
public void dump(@NonNull IndentingPrintWriter ipw) {
if (mMap == null) {
ipw.println("{Empty}");
} else {
for (int i = 0; i < mMap.size(); i++) {
ipw.println("key idx: " + i + "=" + mMap.keyAt(i));
ReferenceWithHistory<V> value = mMap.valueAt(i);
ipw.println("val idx: " + i + "=" + value);
ipw.increaseIndent();
ipw.println("Historic values=[");
ipw.increaseIndent();
value.dump(ipw);
ipw.decreaseIndent();
ipw.println("]");
ipw.decreaseIndent();
}
}
ipw.flush();
}
/**
* Internal method intended for tests that returns the number of historic values associated with
* the supplied key currently. If there is no mapping for the key then {@code 0} is returned.
*/
@VisibleForTesting
public int getHistoryCountForKeyForTests(@Nullable K key) {
if (mMap == null) {
return 0;
}
ReferenceWithHistory<V> valueHolder = mMap.get(key);
if (valueHolder == null) {
return 0;
} else if (valueHolder.getHistoryCount() == 0) {
Log.w(TAG, "getValuesSizeForKeyForTests(\"" + key + "\") was unexpectedly empty");
return 0;
} else {
return valueHolder.getHistoryCount();
}
}
@Override
public String toString() {
return "ArrayMapWithHistory{"
+ "mHistorySize=" + mMaxHistorySize
+ ", mMap=" + mMap
+ '}';
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2019 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.timezonedetector;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.util.IndentingPrintWriter;
import java.util.LinkedList;
/**
* A class that behaves like the following definition, except it stores the history of values set
* that can be dumped for debugging with {@link #dump(IndentingPrintWriter)}.
*
* <pre>{@code
* private static class Ref<V> {
* private V mValue;
*
* public V get() {
* return mValue;
* }
*
* public V set(V value) {
* V previous = mValue;
* mValue = value;
* return previous;
* }
* }
* }</pre>
*
* <p>This class is not thread-safe.
*
* @param <V> the type of the value
*/
public final class ReferenceWithHistory<V> {
/** The size the history linked list is allowed to grow to. */
private final int mMaxHistorySize;
@Nullable
private LinkedList<V> mValues;
/**
* Creates an instance that records, at most, the specified number of values.
*/
public ReferenceWithHistory(@IntRange(from = 1) int maxHistorySize) {
if (maxHistorySize < 1) {
throw new IllegalArgumentException("maxHistorySize < 1: " + maxHistorySize);
}
this.mMaxHistorySize = maxHistorySize;
}
/** Returns the current value, or {@code null} if it has never been set. */
@Nullable
public V get() {
return (mValues == null || mValues.isEmpty()) ? null : mValues.getFirst();
}
/** Sets the current value. Returns the previous value, or {@code null}. */
@Nullable
public V set(@Nullable V newValue) {
if (mValues == null) {
mValues = new LinkedList<>();
}
V previous = get();
mValues.addFirst(newValue);
if (mValues.size() > mMaxHistorySize) {
mValues.removeLast();
}
return previous;
}
/**
* Dumps the content of the reference, including historic values, using the supplied writer.
*/
public void dump(@NonNull IndentingPrintWriter ipw) {
if (mValues == null) {
ipw.println("{Empty}");
} else {
int i = 0;
for (V value : mValues) {
ipw.println(i + ": " + value);
i++;
}
}
ipw.flush();
}
/**
* Returns the number of historic entries stored currently.
*/
public int getHistoryCount() {
return mValues == null ? 0 : mValues.size();
}
@Override
public String toString() {
return String.valueOf(get());
}
}

View File

@@ -27,7 +27,6 @@ import android.annotation.Nullable;
import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.PhoneTimeZoneSuggestion;
import android.content.Context;
import android.util.ArrayMap;
import android.util.LocalLog;
import android.util.Slog;
@@ -38,8 +37,6 @@ import com.android.internal.util.IndentingPrintWriter;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
/**
@@ -175,14 +172,13 @@ public class TimeZoneDetectorStrategy {
private final LocalLog mTimeZoneChangesLog = new LocalLog(30, false /* useLocalTimestamps */);
/**
* A mapping from phoneId to a linked list of phone time zone suggestions (the head being the
* latest). We typically expect one or two entries in this Map: devices will have a small number
* of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
* the ID will not exceed {@link #KEEP_PHONE_SUGGESTION_HISTORY_SIZE} in size.
* A mapping from phoneId to a phone time zone suggestion. We typically expect one or two
* mappings: devices will have a small number of telephony devices and phoneIds are assumed to
* be stable.
*/
@GuardedBy("this")
private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId =
new ArrayMap<>();
private ArrayMapWithHistory<Integer, QualifiedPhoneTimeZoneSuggestion> mSuggestionByPhoneId =
new ArrayMapWithHistory<>(KEEP_PHONE_SUGGESTION_HISTORY_SIZE);
/**
* Creates a new instance of {@link TimeZoneDetectorStrategy}.
@@ -226,16 +222,7 @@ public class TimeZoneDetectorStrategy {
new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
// Store the suggestion against the correct phoneId.
LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
mSuggestionByPhoneId.get(suggestion.getPhoneId());
if (suggestions == null) {
suggestions = new LinkedList<>();
mSuggestionByPhoneId.put(suggestion.getPhoneId(), suggestions);
}
suggestions.addFirst(scoredSuggestion);
if (suggestions.size() > KEEP_PHONE_SUGGESTION_HISTORY_SIZE) {
suggestions.removeLast();
}
mSuggestionByPhoneId.put(suggestion.getPhoneId(), scoredSuggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the time
// zone setting.
@@ -398,13 +385,7 @@ public class TimeZoneDetectorStrategy {
// rate-limit so age is not a strong indicator of confidence. Instead, the callers are
// expected to withdraw suggestions they no longer have confidence in.
for (int i = 0; i < mSuggestionByPhoneId.size(); i++) {
LinkedList<QualifiedPhoneTimeZoneSuggestion> phoneSuggestions =
mSuggestionByPhoneId.valueAt(i);
if (phoneSuggestions == null) {
// Unexpected
continue;
}
QualifiedPhoneTimeZoneSuggestion candidateSuggestion = phoneSuggestions.getFirst();
QualifiedPhoneTimeZoneSuggestion candidateSuggestion = mSuggestionByPhoneId.valueAt(i);
if (candidateSuggestion == null) {
// Unexpected
continue;
@@ -474,16 +455,7 @@ public class TimeZoneDetectorStrategy {
ipw.println("Phone suggestion history:");
ipw.increaseIndent(); // level 2
for (Map.Entry<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> entry
: mSuggestionByPhoneId.entrySet()) {
ipw.println("Phone " + entry.getKey());
ipw.increaseIndent(); // level 3
for (QualifiedPhoneTimeZoneSuggestion suggestion : entry.getValue()) {
ipw.println(suggestion);
}
ipw.decreaseIndent(); // level 3
}
mSuggestionByPhoneId.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
ipw.flush();
@@ -494,12 +466,7 @@ public class TimeZoneDetectorStrategy {
*/
@VisibleForTesting
public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) {
LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
mSuggestionByPhoneId.get(phoneId);
if (suggestions == null) {
return null;
}
return suggestions.getFirst();
return mSuggestionByPhoneId.get(phoneId);
}
/**

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2019 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.timedetector;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import android.util.ArrayMap;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.timezonedetector.ArrayMapWithHistory;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.StringWriter;
import java.util.concurrent.Callable;
@RunWith(AndroidJUnit4.class)
public class ArrayMapWithHistoryTest {
@Test
public void testValueHistoryBehavior() {
// Create a map that will retain 2 values per key.
ArrayMapWithHistory<String, String> historyMap = new ArrayMapWithHistory<>(2 /* history */);
ArrayMap<String, String> arrayMap = new ArrayMap<>();
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", null));
assertEquals(0, historyMap.getHistoryCountForKeyForTests("K1"));
assertToStringAndDumpNotNull(historyMap);
putAndCompareReturnValue(historyMap, arrayMap, "K1", "V1");
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1"));
compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap);
assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1"));
assertToStringAndDumpNotNull(historyMap);
// put() a new value for the same key.
putAndCompareReturnValue(historyMap, arrayMap, "K1", "V2");
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V2"));
compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap);
assertEquals(2, historyMap.getHistoryCountForKeyForTests("K1"));
assertToStringAndDumpNotNull(historyMap);
// put() a new value for the same key. We should have hit the limit of "2 values retained
// per key".
putAndCompareReturnValue(historyMap, arrayMap, "K1", "V3");
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V3"));
compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap);
assertEquals(2, historyMap.getHistoryCountForKeyForTests("K1"));
assertToStringAndDumpNotNull(historyMap);
}
@Test
public void testMapBehavior() throws Exception {
ArrayMapWithHistory<String, String> historyMap = new ArrayMapWithHistory<>(2);
ArrayMap<String, String> arrayMap = new ArrayMap<>();
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", null), entry("K2", null));
assertIndexAccessThrowsException(0, historyMap, arrayMap);
assertEquals(0, historyMap.getHistoryCountForKeyForTests("K1"));
assertEquals(0, historyMap.getHistoryCountForKeyForTests("K2"));
putAndCompareReturnValue(historyMap, arrayMap, "K1", "V1");
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1"), entry("K2", null));
compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap);
// TODO Restore after http://b/146563025 is fixed and ArrayMap behaves properly in tests.
// assertIndexAccessThrowsException(1, historyMap, arrayMap);
assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1"));
assertToStringAndDumpNotNull(historyMap);
putAndCompareReturnValue(historyMap, arrayMap, "K2", "V2");
compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1"), entry("K2", "V2"));
compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap);
compareKeyAtAndValueAtForIndex(1, historyMap, arrayMap);
// TODO Restore after http://b/146563025 is fixed and ArrayMap behaves properly in tests.
// assertIndexAccessThrowsException(2, historyMap, arrayMap);
assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1"));
assertEquals(1, historyMap.getHistoryCountForKeyForTests("K2"));
assertToStringAndDumpNotNull(historyMap);
}
private static String dumpHistoryMap(ArrayMapWithHistory<?, ?> historyMap) {
StringWriter stringWriter = new StringWriter();
try (IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ")) {
historyMap.dump(ipw);
return stringWriter.toString();
}
}
private static <K, V> void putAndCompareReturnValue(ArrayMapWithHistory<K, V> historyMap,
ArrayMap<K, V> arrayMap, K key, V value) {
assertEquals(arrayMap.put(key, value), historyMap.put(key, value));
}
private static class Entry<K, V> {
public final K key;
public final V value;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
}
private static <K, V> Entry<K, V> entry(K key, V value) {
return new Entry<>(key, value);
}
@SafeVarargs
private static <K, V> void compareGetAndSizeForKeys(ArrayMapWithHistory<K, V> historyMap,
ArrayMap<K, V> arrayMap, Entry<K, V>... expectedEntries) {
for (Entry<K, V> expectedEntry : expectedEntries) {
assertEquals(arrayMap.get(expectedEntry.key), historyMap.get(expectedEntry.key));
assertEquals(expectedEntry.value, historyMap.get(expectedEntry.key));
}
assertEquals(arrayMap.size(), historyMap.size());
}
private static void compareKeyAtAndValueAtForIndex(
int index, ArrayMapWithHistory<?, ?> historyMap, ArrayMap<?, ?> arrayMap) {
assertEquals(arrayMap.keyAt(index), historyMap.keyAt(index));
assertEquals(arrayMap.valueAt(index), historyMap.valueAt(index));
}
private static void assertIndexAccessThrowsException(
int index, ArrayMapWithHistory<?, ?> historyMap, ArrayMap<?, ?> arrayMap)
throws Exception {
assertThrowsArrayIndexOutOfBoundsException(
"ArrayMap.keyAt(" + index + ")", () -> arrayMap.keyAt(index));
assertThrowsArrayIndexOutOfBoundsException(
"ArrayMapWithHistory.keyAt(" + index + ")", () -> historyMap.keyAt(index));
assertThrowsArrayIndexOutOfBoundsException(
"ArrayMap.keyAt(" + index + ")", () -> arrayMap.valueAt(index));
assertThrowsArrayIndexOutOfBoundsException(
"ArrayMapWithHistory.keyAt(" + index + ")", () -> historyMap.valueAt(index));
}
private static void assertThrowsArrayIndexOutOfBoundsException(
String description, Callable<?> callable) throws Exception {
try {
callable.call();
fail("Expected exception for " + description);
} catch (ArrayIndexOutOfBoundsException expected) {
// This is fine.
} catch (Exception e) {
// Any other exception is just rethrown.
throw e;
}
}
private static void assertToStringAndDumpNotNull(ArrayMapWithHistory<?, ?> historyMap) {
assertNotNull(historyMap.toString());
assertNotNull(dumpHistoryMap(historyMap));
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2019 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.timedetector;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.timezonedetector.ReferenceWithHistory;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.StringWriter;
@RunWith(AndroidJUnit4.class)
public class ReferenceWithHistoryTest {
@Test
public void testBasicReferenceBehavior() {
// Create a reference that will retain 2 history values.
ReferenceWithHistory<String> referenceWithHistory =
new ReferenceWithHistory<>(2 /* history */);
TestRef<String> reference = new TestRef<>();
// Check unset behavior.
compareGet(referenceWithHistory, reference, null);
assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
compareToString(referenceWithHistory, reference, "null");
// Try setting null.
setAndCompareReturnValue(referenceWithHistory, reference, null);
compareGet(referenceWithHistory, reference, null);
assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
compareToString(referenceWithHistory, reference, "null");
// Try setting a non-null value.
setAndCompareReturnValue(referenceWithHistory, reference, "Foo");
compareGet(referenceWithHistory, reference, "Foo");
assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
compareToString(referenceWithHistory, reference, "Foo");
// Try setting null again.
setAndCompareReturnValue(referenceWithHistory, reference, "Foo");
compareGet(referenceWithHistory, reference, "Foo");
assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
compareToString(referenceWithHistory, reference, "Foo");
// Try a non-null value again.
setAndCompareReturnValue(referenceWithHistory, reference, "Bar");
compareGet(referenceWithHistory, reference, "Bar");
assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
compareToString(referenceWithHistory, reference, "Bar");
}
@Test
public void testValueHistoryBehavior() {
// Create a reference that will retain 2 history values.
ReferenceWithHistory<String> referenceWithHistory =
new ReferenceWithHistory<>(2 /* history */);
TestRef<String> reference = new TestRef<>();
// Assert behavior before anything is set.
assertEquals(0, referenceWithHistory.getHistoryCount());
// Set a value (1).
setAndCompareReturnValue(referenceWithHistory, reference, "V1");
assertEquals(1, referenceWithHistory.getHistoryCount());
// Set a value (2).
setAndCompareReturnValue(referenceWithHistory, reference, "V2");
assertEquals(2, referenceWithHistory.getHistoryCount());
// Set a value (3).
// We should have hit the limit of "2 history values retained per key".
setAndCompareReturnValue(referenceWithHistory, reference, "V3");
assertEquals(2, referenceWithHistory.getHistoryCount());
}
/**
* A simple class that has the same behavior as ReferenceWithHistory without the history. Used
* in tests for comparison.
*/
private static class TestRef<V> {
private V mValue;
public V get() {
return mValue;
}
public V set(V value) {
V previous = mValue;
mValue = value;
return previous;
}
public String toString() {
return String.valueOf(mValue);
}
}
private static void compareGet(
ReferenceWithHistory<?> referenceWithHistory, TestRef<?> reference, Object value) {
assertEquals(reference.get(), referenceWithHistory.get());
assertEquals(value, reference.get());
}
private static <T> void setAndCompareReturnValue(
ReferenceWithHistory<T> referenceWithHistory, TestRef<T> reference, T newValue) {
assertEquals(reference.set(newValue), referenceWithHistory.set(newValue));
}
private static void compareToString(
ReferenceWithHistory<?> referenceWithHistory, TestRef<?> reference, String expected) {
assertEquals(reference.toString(), referenceWithHistory.toString());
assertEquals(expected, referenceWithHistory.toString());
}
private static String dumpReferenceWithHistory(ReferenceWithHistory<?> referenceWithHistory) {
StringWriter stringWriter = new StringWriter();
try (IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ")) {
referenceWithHistory.dump(ipw);
return stringWriter.toString();
}
}
}