Add tests for PerformBackupTask

Tests for agent/transport interaction and some more.

Test: m -j RunFrameworksServicesRoboTests
Change-Id: Ie90044bdbdd32e6cfa70d6228841fec2d9fb0188
This commit is contained in:
Bernardo Rufino
2018-02-08 15:26:15 +00:00
parent 44dbca0999
commit b97ea7ea23
3 changed files with 251 additions and 38 deletions

View File

@@ -26,6 +26,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -40,7 +42,9 @@ import android.app.Application;
import android.app.IActivityManager;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackupDataOutput;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupManagerMonitor;
@@ -53,6 +57,7 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
@@ -80,6 +85,8 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
@@ -88,6 +95,7 @@ import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowQueuedWork;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@@ -184,32 +192,33 @@ public class PerformBackupTaskTest {
@Test
public void testRunTask_whenTransportProvidesFlags_passesThemToTheAgent() throws Exception {
BackupAgent agent = setUpAgent(PACKAGE_1);
AgentMock agentMock = setUpAgent(PACKAGE_1);
int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
when(mTransportBinder.getTransportFlags()).thenReturn(flags);
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
runTask(task);
verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
verify(agentMock.agent)
.onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
}
@Test
public void testRunTask_whenTransportDoesNotProvidesFlags() throws Exception {
BackupAgent agent = setUpAgent(PACKAGE_1);
AgentMock agentMock = setUpAgent(PACKAGE_1);
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
runTask(task);
verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any());
verify(agentMock.agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any());
}
@Test
public void testRunTask_whenTransportProvidesFlagsAndMultipleAgents_passesToAll()
throws Exception {
List<BackupAgent> agents = setUpAgents(PACKAGE_1, PACKAGE_2);
BackupAgent agent1 = agents.get(0);
BackupAgent agent2 = agents.get(1);
List<AgentMock> agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2);
BackupAgent agent1 = agentMocks.get(0).agent;
BackupAgent agent2 = agentMocks.get(1).agent;
int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
when(mTransportBinder.getTransportFlags()).thenReturn(flags);
PerformBackupTask task =
@@ -223,14 +232,103 @@ public class PerformBackupTaskTest {
@Test
public void testRunTask_whenTransportChangeFlagsAfterTaskCreation() throws Exception {
BackupAgent agent = setUpAgent(PACKAGE_1);
AgentMock agentMock = setUpAgent(PACKAGE_1);
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
when(mTransportBinder.getTransportFlags()).thenReturn(flags);
runTask(task);
verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
verify(agentMock.agent)
.onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any());
}
@Test
public void testRunTask_callsListenerOnTaskFinished() throws Exception {
setUpAgent(PACKAGE_1);
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
runTask(task);
verify(mListener).onFinished(any());
}
@Test
public void testRunTask_callsTransportPerformBackup() throws Exception {
AgentMock agentMock = setUpAgent(PACKAGE_1);
agentOnBackupDo(
agentMock.agent,
(oldState, dataOutput, newState) -> {
writeData(dataOutput, "key1", "foo".getBytes());
writeData(dataOutput, "key2", "bar".getBytes());
});
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
// We need to verify at call time because the file is deleted right after
when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
.then(this::mockAndVerifyTransportPerformBackupData);
runTask(task);
// Already verified data in mockAndVerifyPerformBackupData
verify(mTransportBinder).performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
}
private int mockAndVerifyTransportPerformBackupData(InvocationOnMock invocation)
throws IOException {
ParcelFileDescriptor data = invocation.getArgument(1);
// Verifying that what we passed to the transport is what the agent wrote
BackupDataInput dataInput = new BackupDataInput(data.getFileDescriptor());
// "key1" => "foo"
assertThat(dataInput.readNextHeader()).isTrue();
assertThat(dataInput.getKey()).isEqualTo("key1");
int size1 = dataInput.getDataSize();
byte[] data1 = new byte[size1];
dataInput.readEntityData(data1, 0, size1);
assertThat(data1).isEqualTo("foo".getBytes());
// "key2" => "bar"
assertThat(dataInput.readNextHeader()).isTrue();
assertThat(dataInput.getKey()).isEqualTo("key2");
int size2 = dataInput.getDataSize();
byte[] data2 = new byte[size2];
dataInput.readEntityData(data2, 0, size2);
assertThat(data2).isEqualTo("bar".getBytes());
// No more
assertThat(dataInput.readNextHeader()).isFalse();
return BackupTransport.TRANSPORT_OK;
}
@Test
public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup()
throws Exception {
setUpAgent(PACKAGE_1);
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
.thenReturn(BackupTransport.TRANSPORT_OK);
runTask(task);
verify(mTransportBinder).finishBackup();
}
@Test
public void testRunTask_whenProhibitedKey_failsAgent() throws Exception {
AgentMock agentMock = setUpAgent(PACKAGE_1);
agentOnBackupDo(
agentMock.agent,
(oldState, dataOutput, newState) -> {
char prohibitedChar = 0xff00;
writeData(dataOutput, prohibitedChar + "key", "foo".getBytes());
});
PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1);
runTask(task);
verify(agentMock.agentBinder).fail(any());
}
private void runTask(PerformBackupTask task) {
@@ -241,26 +339,34 @@ public class PerformBackupTaskTest {
}
}
private List<BackupAgent> setUpAgents(String... packageNames) {
private List<AgentMock> setUpAgents(String... packageNames) {
return Stream.of(packageNames).map(this::setUpAgent).collect(toList());
}
private BackupAgent setUpAgent(String packageName) {
PackageInfo packageInfo = new PackageInfo();
packageInfo.packageName = packageName;
packageInfo.applicationInfo = new ApplicationInfo();
packageInfo.applicationInfo.flags = ApplicationInfo.FLAG_ALLOW_BACKUP;
packageInfo.applicationInfo.backupAgentName = "BackupAgent" + packageName;
packageInfo.applicationInfo.packageName = packageName;
mShadowPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
mShadowPackageManager.addPackage(packageInfo);
BackupAgent backupAgent = spy(BackupAgent.class);
IBackupAgent backupAgentBinder = IBackupAgent.Stub.asInterface(backupAgent.onBind());
when(mBackupManagerService.bindToAgentSynchronous(
eq(packageInfo.applicationInfo), anyInt()))
.thenReturn(backupAgentBinder);
return backupAgent;
private AgentMock setUpAgent(String packageName) {
try {
PackageInfo packageInfo = new PackageInfo();
packageInfo.packageName = packageName;
packageInfo.applicationInfo = new ApplicationInfo();
packageInfo.applicationInfo.flags = ApplicationInfo.FLAG_ALLOW_BACKUP;
packageInfo.applicationInfo.backupAgentName = "BackupAgent" + packageName;
packageInfo.applicationInfo.packageName = packageName;
mShadowPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
mShadowPackageManager.addPackage(packageInfo);
BackupAgent backupAgent = spy(BackupAgent.class);
IBackupAgent backupAgentBinder =
spy(IBackupAgent.Stub.asInterface(backupAgent.onBind()));
// Don't crash our only process (in production code this would crash the app, not us)
doNothing().when(backupAgentBinder).fail(any());
when(mBackupManagerService.bindToAgentSynchronous(
eq(packageInfo.applicationInfo), anyInt()))
.thenReturn(backupAgentBinder);
return new AgentMock(backupAgentBinder, backupAgent);
} catch (RemoteException e) {
// Never happens, compiler happy
throw new AssertionError(e);
}
}
private PerformBackupTask createPerformBackupTask(
@@ -288,10 +394,53 @@ public class PerformBackupTaskTest {
return task;
}
private ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) {
private static ArgumentMatcher<PackageInfo> packageInfo(String packageName) {
return packageInfo -> packageName.equals(packageInfo.packageName);
}
private static ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) {
return dataOutput -> dataOutput.getTransportFlags() == flags;
}
private static void writeData(BackupDataOutput dataOutput, String key, byte[] data)
throws IOException {
dataOutput.writeEntityHeader(key, data.length);
dataOutput.writeEntityData(data, data.length);
}
private static void agentOnBackupDo(BackupAgent agent, BackupAgentOnBackup function)
throws Exception {
doAnswer(function).when(agent).onBackup(any(), any(), any());
}
@FunctionalInterface
private interface BackupAgentOnBackup extends Answer<Void> {
void onBackup(
ParcelFileDescriptor oldState,
BackupDataOutput dataOutput,
ParcelFileDescriptor newState)
throws IOException;
@Override
default Void answer(InvocationOnMock invocation) throws Throwable {
onBackup(
invocation.getArgument(0),
invocation.getArgument(1),
invocation.getArgument(2));
return null;
}
}
private static class AgentMock {
private final IBackupAgent agentBinder;
private final BackupAgent agent;
public AgentMock(IBackupAgent agentBinder, BackupAgent agent) {
this.agentBinder = agentBinder;
this.agent = agent;
}
}
private abstract static class FakeIBackupManager extends IBackupManager.Stub {
private Handler mBackupHandler;
private BackupRestoreTask mTask;

View File

@@ -21,41 +21,76 @@ import android.app.backup.BackupDataInput;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.io.EOFException;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
* Shadow for {@link BackupDataInput}. Format read does NOT match implementation. To write data to
* be read by this shadow, you should also declare shadow {@link ShadowBackupDataOutput}.
*/
@Implements(BackupDataInput.class)
public class ShadowBackupDataInput {
@Implementation
public void __constructor__(FileDescriptor fd) {
}
private ObjectInputStream mInput;
private int mSize;
private String mKey;
private boolean mHeaderReady;
@Implementation
protected void finalize() throws Throwable {
public void __constructor__(FileDescriptor fd) {
try {
mInput = new ObjectInputStream(new FileInputStream(fd));
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Implementation
public boolean readNextHeader() throws IOException {
return false;
mHeaderReady = false;
try {
mSize = mInput.readInt();
} catch (EOFException e) {
return false;
}
mKey = mInput.readUTF();
mHeaderReady = true;
return true;
}
@Implementation
public String getKey() {
throw new AssertionError("Can't call because readNextHeader() returned false");
checkHeaderReady();
return mKey;
}
@Implementation
public int getDataSize() {
throw new AssertionError("Can't call because readNextHeader() returned false");
checkHeaderReady();
return mSize;
}
@Implementation
public int readEntityData(byte[] data, int offset, int size) throws IOException {
throw new AssertionError("Can't call because readNextHeader() returned false");
checkHeaderReady();
int result = mInput.read(data, offset, size);
if (result < 0) {
throw new IOException("result=0x" + Integer.toHexString(result));
}
return result;
}
@Implementation
public void skipEntityData() throws IOException {
throw new AssertionError("Can't call because readNextHeader() returned false");
checkHeaderReady();
mInput.read(new byte[mSize], 0, mSize);
}
private void checkHeaderReady() {
if (!mHeaderReady) {
throw new IllegalStateException("Entity header not read");
}
}
}

View File

@@ -21,16 +21,29 @@ import android.app.backup.BackupDataOutput;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* Shadow for {@link BackupDataOutput}. Format written does NOT match implementation. To read data
* written with this shadow you should also declare shadow {@link ShadowBackupDataInput}.
*/
@Implements(BackupDataOutput.class)
public class ShadowBackupDataOutput {
private long mQuota;
private int mTransportFlags;
private ObjectOutputStream mOutput;
@Implementation
public void __constructor__(FileDescriptor fd, long quota, int transportFlags) {
try {
mOutput = new ObjectOutputStream(new FileOutputStream(fd));
} catch (IOException e) {
throw new AssertionError(e);
}
mQuota = quota;
mTransportFlags = transportFlags;
}
@@ -47,11 +60,27 @@ public class ShadowBackupDataOutput {
@Implementation
public int writeEntityHeader(String key, int dataSize) throws IOException {
return 0;
final int size;
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {
writeEntityHeader(new ObjectOutputStream(byteStream), key, dataSize);
size = byteStream.size();
}
writeEntityHeader(mOutput, key, dataSize);
return size;
}
private void writeEntityHeader(ObjectOutputStream stream, String key, int dataSize)
throws IOException {
// Write the int first because readInt() throws EOFException, to know when stream ends
stream.writeInt(dataSize);
stream.writeUTF(key);
stream.flush();
}
@Implementation
public int writeEntityData(byte[] data, int size) throws IOException {
return 0;
mOutput.write(data, 0, size);
mOutput.flush();
return size;
}
}