BaseBundle.java: Adding tests for 'Recycle underlying parcel for bundle'

Follow up to ag/18795008. Adding tests for new behaviour.

As it is not possible to directly check on the bundle if the parcel has been recycled,
we inject a spy into it. Mocking static methods is not possible in
FrameworkCoreTests, so we introduce a new test class in
FrameworksMockingCoreTests which is run when any parcel or bundle source
file changes.

Bug: 233216232
Test: atest BundleRecyclingTest

Change-Id: If691b68a8db1f0110d131ec620935f566cc9367b
This commit is contained in:
Hani Kazmi
2022-06-19 16:30:44 +00:00
parent 81222d2803
commit 621dc99968
2 changed files with 268 additions and 0 deletions

View File

@@ -57,6 +57,18 @@
{ "include-filter": "com.android.server.am.BatteryExternalStatsWorkerTest" }
]
},
{
"file_patterns": [
"Parcel\\.java",
"[^/]*Bundle[^/]*\\.java"
],
"name": "FrameworksMockingCoreTests",
"options": [
{ "include-filter": "android.os.BundleRecyclingTest"},
{ "exclude-annotation": "androidx.test.filters.FlakyTest" },
{ "exclude-annotation": "org.junit.Ignore" }
]
},
{
"file_patterns": [
"BatteryUsageStats[^/]*\\.java",

View File

@@ -0,0 +1,256 @@
/*
* Copyright (C) 2022 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 android.os;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed.
*
* <p>Build/Install/Run:
* atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class BundleRecyclingTest {
private Parcel mParcelSpy;
private Bundle mBundle;
@Before
public void setUp() throws Exception {
setUpBundle(/* hasLazy */ true);
}
@Test
public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() {
setUpBundle(/* hasLazy */ false);
// Will unparcel and immediately recycle parcel
assertNotNull(mBundle.getString("key"));
verify(mParcelSpy, times(1)).recycle();
assertFalse(mBundle.isDefinitelyEmpty());
// Should not recycle again
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
}
@Test
public void bundleClear_whenParcelled_recyclesParcel() {
assertTrue(mBundle.isParcelled());
verify(mParcelSpy, times(0)).recycle();
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
// Should not recycle again
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
// Will unparcel with a lazy value, and immediately unwrap the lazy value,
// with no lazy values left at the end of getParcelable
assertNotNull(mBundle.getParcelable("key", CustomParcelable.class));
verify(mParcelSpy, times(0)).recycle();
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
// Should not recycle again
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() {
// Will unparcel but keep the CustomParcelable lazy
assertFalse(mBundle.isEmpty());
verify(mParcelSpy, times(0)).recycle();
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
// Should not recycle again
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() {
Bundle copy = new Bundle();
copy.putAll(mBundle);
mBundle.clear();
assertTrue(mBundle.isDefinitelyEmpty());
copy.clear();
assertTrue(copy.isDefinitelyEmpty());
verify(mParcelSpy, never()).recycle();
}
@Test
public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() {
// Will unparcel but keep the CustomParcelable lazy
assertFalse(mBundle.isEmpty());
Bundle copy = mBundle.deepCopy();
copy.putAll(mBundle);
mBundle.clear();
assertTrue(mBundle.isDefinitelyEmpty());
copy.clear();
assertTrue(copy.isDefinitelyEmpty());
verify(mParcelSpy, never()).recycle();
}
private void setUpBundle(boolean hasLazy) {
AtomicReference<Parcel> parcel = new AtomicReference<>();
StaticMockitoSession session = mockitoSession()
.strictness(Strictness.STRICT_STUBS)
.spyStatic(Parcel.class)
.startMocking();
doAnswer((Answer<Parcel>) invocationOnSpy -> {
Parcel spy = (Parcel) invocationOnSpy.callRealMethod();
spyOn(spy);
parcel.set(spy);
return spy;
}).when(() -> Parcel.obtain());
Bundle bundle = new Bundle();
bundle.setClassLoader(getClass().getClassLoader());
Parcel p = createBundle(hasLazy);
bundle.readFromParcel(p);
p.recycle();
session.finishMocking();
mParcelSpy = parcel.get();
mBundle = bundle;
}
/**
* Create a test bundle, parcel it and return the parcel.
*/
private Parcel createBundle(boolean hasLazy) {
final Bundle source = new Bundle();
if (hasLazy) {
source.putParcelable("key", new CustomParcelable(13, "Tiramisu"));
} else {
source.putString("key", "tiramisu");
}
return getParcelledBundle(source);
}
/**
* Take a bundle, write it to a parcel and return the parcel.
*/
private Parcel getParcelledBundle(Bundle bundle) {
final Parcel p = Parcel.obtain();
// Don't use p.writeParcelabe(), which would write the creator, which we don't need.
bundle.writeToParcel(p, 0);
p.setDataPosition(0);
return p;
}
private static class CustomParcelable implements Parcelable {
public final int integer;
public final String string;
CustomParcelable(int integer, String string) {
this.integer = integer;
this.string = string;
}
protected CustomParcelable(Parcel in) {
integer = in.readInt();
string = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(integer);
out.writeString(string);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof CustomParcelable)) {
return false;
}
CustomParcelable that = (CustomParcelable) other;
return integer == that.integer && string.equals(that.string);
}
@Override
public int hashCode() {
return Objects.hash(integer, string);
}
public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() {
@Override
public CustomParcelable createFromParcel(Parcel in) {
return new CustomParcelable(in);
}
@Override
public CustomParcelable[] newArray(int size) {
return new CustomParcelable[size];
}
};
}
}