OSDN Git Service

Increase PerformBackupTask unit coverage
authorBernardo Rufino <brufino@google.com>
Thu, 28 Jun 2018 07:30:14 +0000 (08:30 +0100)
committerBernardo Rufino <brufino@google.com>
Tue, 17 Jul 2018 11:23:11 +0000 (12:23 +0100)
With KV Refactor in mind.
* Added tests around empty queue and single package backups.
* Refactored a bit some of the existing tests.
* Moved from mocking BMS to using a real instance and had to adjust a
  few things for this.

Test: atest PerformBackupTaskTest
Change-Id: I0ee3be32c7cbac5ed2cdc2717408749907c15ade

services/backup/java/com/android/server/backup/BackupManagerService.java
services/backup/java/com/android/server/backup/internal/PerformBackupTask.java
services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
services/robotests/src/android/app/backup/ForwardingBackupAgent.java [new file with mode: 0644]
services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
services/robotests/src/com/android/server/backup/Utils.java [new file with mode: 0644]
services/robotests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
services/robotests/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
services/robotests/src/com/android/server/backup/testing/TransportTestUtils.java

index cd1bd67..574288a 100644 (file)
@@ -38,6 +38,7 @@ import android.app.AppGlobals;
 import android.app.IActivityManager;
 import android.app.IBackupAgent;
 import android.app.PendingIntent;
+import android.app.backup.BackupAgent;
 import android.app.backup.BackupManager;
 import android.app.backup.BackupManagerMonitor;
 import android.app.backup.FullBackup;
@@ -704,7 +705,7 @@ public class BackupManagerService implements BackupManagerServiceInterface {
      * process-local non-lifecycle agent instance, so we manually set up the context
      * topology for it.
      */
-    public PackageManagerBackupAgent makeMetadataAgent() {
+    public BackupAgent makeMetadataAgent() {
         PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent(mPackageManager);
         pmAgent.attach(mContext);
         pmAgent.onCreate();
@@ -784,7 +785,7 @@ public class BackupManagerService implements BackupManagerServiceInterface {
     }
 
     @VisibleForTesting
-    BackupManagerService(
+    public BackupManagerService(
             Context context,
             Trampoline parent,
             HandlerThread backupThread,
@@ -1749,6 +1750,16 @@ public class BackupManagerService implements BackupManagerServiceInterface {
         }
     }
 
+    public void putOperation(int token, Operation operation) {
+        if (MORE_DEBUG) {
+            Slog.d(TAG, "Adding operation token=" + Integer.toHexString(token) + ", operation type="
+                    + operation.type);
+        }
+        synchronized (mCurrentOpLock) {
+            mCurrentOperations.put(token, operation);
+        }
+    }
+
     public void removeOperation(int token) {
         if (MORE_DEBUG) {
             Slog.d(TAG, "Removing operation token=" + Integer.toHexString(token));
index ae43299..90b4bbd 100644 (file)
@@ -30,6 +30,7 @@ import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_RESTOR
 import android.annotation.Nullable;
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
+import android.app.backup.BackupAgent;
 import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupManager;
@@ -205,10 +206,8 @@ public class PerformBackupTask implements BackupRestoreTask {
      * Put this task in the repository of running tasks.
      */
     private void registerTask() {
-        synchronized (backupManagerService.getCurrentOpLock()) {
-            backupManagerService.getCurrentOperations().put(
-                    mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
-        }
+        backupManagerService.putOperation(
+                mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
     }
 
     /**
@@ -358,7 +357,7 @@ public class PerformBackupTask implements BackupRestoreTask {
             // The package manager doesn't have a proper <application> etc, but since it's running
             // here in the system process we can just set up its agent directly and use a synthetic
             // BackupRequest.
-            PackageManagerBackupAgent pmAgent = backupManagerService.makeMetadataAgent();
+            BackupAgent pmAgent = backupManagerService.makeMetadataAgent();
             mStatus = invokeAgentForBackup(
                     PACKAGE_MANAGER_SENTINEL,
                     IBackupAgent.Stub.asInterface(pmAgent.onBind()));
index 78b000d..32dbad9 100644 (file)
@@ -30,6 +30,7 @@ import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERA
 
 
 import android.app.IBackupAgent;
+import android.app.backup.BackupAgent;
 import android.app.backup.IFullBackupRestoreObserver;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.Signature;
@@ -76,7 +77,7 @@ public class PerformAdbRestoreTask implements Runnable {
     private final String mCurrentPassword;
     private final String mDecryptPassword;
     private final AtomicBoolean mLatchObject;
-    private final PackageManagerBackupAgent mPackageManagerBackupAgent;
+    private final BackupAgent mPackageManagerBackupAgent;
     private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
 
     private IFullBackupRestoreObserver mObserver;
diff --git a/services/robotests/src/android/app/backup/ForwardingBackupAgent.java b/services/robotests/src/android/app/backup/ForwardingBackupAgent.java
new file mode 100644 (file)
index 0000000..4ff5b7c
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 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.app.backup;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Useful for spying in {@link BackupAgent} instances since their {@link BackupAgent#onBind()} is
+ * final and always points to the original instance, instead of the spy.
+ *
+ * <p>To use, construct a spy of the desired {@link BackupAgent}, spying on the methods of interest.
+ * Then, where you need to pass the agent, use {@link ForwardingBackupAgent#forward(BackupAgent)}
+ * with the spy.
+ */
+public class ForwardingBackupAgent extends BackupAgent {
+    /** Returns a {@link BackupAgent} that forwards method calls to {@code backupAgent}. */
+    public static BackupAgent forward(BackupAgent backupAgent) {
+        return new ForwardingBackupAgent(backupAgent);
+    }
+
+    private final BackupAgent mBackupAgent;
+
+    private ForwardingBackupAgent(BackupAgent backupAgent) {
+        mBackupAgent = backupAgent;
+    }
+
+    @Override
+    public void onCreate() {
+        mBackupAgent.onCreate();
+    }
+
+    @Override
+    public void onDestroy() {
+        mBackupAgent.onDestroy();
+    }
+
+    @Override
+    public void onBackup(
+            ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)
+            throws IOException {
+        mBackupAgent.onBackup(oldState, data, newState);
+    }
+
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+            throws IOException {
+        mBackupAgent.onRestore(data, appVersionCode, newState);
+    }
+
+    @Override
+    public void onRestore(BackupDataInput data, long appVersionCode, ParcelFileDescriptor newState)
+            throws IOException {
+        mBackupAgent.onRestore(data, appVersionCode, newState);
+    }
+
+    @Override
+    public void onFullBackup(FullBackupDataOutput data) throws IOException {
+        mBackupAgent.onFullBackup(data);
+    }
+
+    @Override
+    public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
+        mBackupAgent.onQuotaExceeded(backupDataBytes, quotaBytes);
+    }
+
+    @Override
+    public void onRestoreFile(
+            ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime)
+            throws IOException {
+        mBackupAgent.onRestoreFile(data, size, destination, type, mode, mtime);
+    }
+
+    @Override
+    protected void onRestoreFile(
+            ParcelFileDescriptor data,
+            long size,
+            int type,
+            String domain,
+            String path,
+            long mode,
+            long mtime)
+            throws IOException {
+        mBackupAgent.onRestoreFile(data, size, type, domain, path, mode, mtime);
+    }
+
+    @Override
+    public void onRestoreFinished() {
+        mBackupAgent.onRestoreFinished();
+    }
+
+    @Override
+    public void attach(Context context) {
+        mBackupAgent.attach(context);
+    }
+}
index d16bc26..abaed7c 100644 (file)
@@ -16,7 +16,7 @@
 
 package com.android.server.backup;
 
-import static com.android.server.backup.testing.BackupManagerServiceTestUtils.startBackupThread;
+import static com.android.server.backup.testing.BackupManagerServiceTestUtils.startSilentBackupThread;
 import static com.android.server.backup.testing.TransportData.backupTransport;
 import static com.android.server.backup.testing.TransportData.d2dTransport;
 import static com.android.server.backup.testing.TransportData.localTransport;
@@ -46,6 +46,7 @@ import android.os.PowerSaveState;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
 import com.android.server.backup.internal.BackupRequest;
+import com.android.server.backup.testing.BackupManagerServiceTestUtils;
 import com.android.server.backup.testing.TransportData;
 import com.android.server.backup.testing.TransportTestUtils.TransportMock;
 import com.android.server.backup.transport.TransportNotRegisteredException;
@@ -68,7 +69,6 @@ import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.Implements;
 import org.robolectric.shadows.ShadowContextWrapper;
-import org.robolectric.shadows.ShadowLog;
 import org.robolectric.shadows.ShadowLooper;
 import org.robolectric.shadows.ShadowPackageManager;
 import org.robolectric.shadows.ShadowSettings;
@@ -104,7 +104,10 @@ public class BackupManagerServiceTest {
         mTransport = backupTransport();
         mTransportName = mTransport.transportName;
 
-        mBackupThread = startBackupThread(this::uncaughtException);
+        // Unrelated exceptions are thrown in the backup thread. Until we mock everything properly
+        // we should not fail tests because of this. This is not flakiness, the exceptions thrown
+        // don't interfere with the tests.
+        mBackupThread = startSilentBackupThread(TAG);
         mShadowBackupLooper = shadowOf(mBackupThread.getLooper());
 
         ContextWrapper context = RuntimeEnvironment.application;
@@ -113,8 +116,10 @@ public class BackupManagerServiceTest {
         mShadowContext = shadowOf(context);
 
         File cacheDir = mContext.getCacheDir();
-        mBaseStateDir = new File(cacheDir, "base_state_dir");
-        mDataDir = new File(cacheDir, "data_dir");
+        // Corresponds to /data/backup
+        mBaseStateDir = new File(cacheDir, "base_state");
+        // Corresponds to /cache/backup_stage
+        mDataDir = new File(cacheDir, "data");
 
         ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
     }
@@ -126,13 +131,6 @@ public class BackupManagerServiceTest {
         ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
     }
 
-    private void uncaughtException(Thread thread, Throwable e) {
-        // Unrelated exceptions are thrown in the backup thread. Until we mock everything properly
-        // we should not fail tests because of this. This is not flakiness, the exceptions thrown
-        // don't interfere with the tests.
-        ShadowLog.e(TAG, "Uncaught exception in test thread " + thread.getName(), e);
-    }
-
     /* Tests for destination string */
 
     @Test
@@ -864,22 +862,8 @@ public class BackupManagerServiceTest {
     }
 
     private BackupManagerService createInitializedBackupManagerService() {
-        BackupManagerService backupManagerService =
-                new BackupManagerService(
-                        mContext,
-                        new Trampoline(mContext),
-                        mBackupThread,
-                        mBaseStateDir,
-                        mDataDir,
-                        mTransportManager);
-        mShadowBackupLooper.runToEndOfTasks();
-        // Handler instances have their own clock, so advancing looper (with runToEndOfTasks())
-        // above does NOT advance the handlers' clock, hence whenever a handler post messages with
-        // specific time to the looper the time of those messages will be before the looper's time.
-        // To fix this we advance SystemClock as well since that is from where the handlers read
-        // time.
-        ShadowSystemClock.setCurrentTimeMillis(mShadowBackupLooper.getScheduler().getCurrentTime());
-        return backupManagerService;
+        return BackupManagerServiceTestUtils.createInitializedBackupManagerService(
+                mContext, mBackupThread, mBaseStateDir, mDataDir, mTransportManager);
     }
 
     private void setUpPowerManager(BackupManagerService backupManagerService) {
index 88a51a5..2d6dd4d 100644 (file)
 
 package com.android.server.backup;
 
+import static android.app.backup.ForwardingBackupAgent.forward;
+
+import static com.android.server.backup.BackupManagerService.PACKAGE_MANAGER_SENTINEL;
 import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createBackupWakeLock;
+import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createInitializedBackupManagerService;
 import static com.android.server.backup.testing.BackupManagerServiceTestUtils.setUpBackupManagerServiceBasics;
-import static com.android.server.backup.testing.BackupManagerServiceTestUtils.startBackupThreadAndGetLooper;
 import static com.android.server.backup.testing.TestUtils.uncheck;
 import static com.android.server.backup.testing.TransportData.backupTransport;
 import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static java.util.Collections.emptyList;
 import static java.util.stream.Collectors.toCollection;
 import static java.util.stream.Collectors.toList;
@@ -34,6 +39,7 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.intThat;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -51,12 +57,12 @@ import android.app.backup.BackupTransport;
 import android.app.backup.IBackupManager;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IBackupObserver;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.os.DeadObjectException;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.PowerManager;
@@ -77,13 +83,23 @@ import com.android.server.testing.SystemLoaderClasses;
 import com.android.server.testing.SystemLoaderPackages;
 import com.android.server.testing.shadows.ShadowBackupDataInput;
 import com.android.server.testing.shadows.ShadowBackupDataOutput;
+
+import com.google.common.truth.IterableSubject;
+
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.stream.Stream;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -98,6 +114,19 @@ import org.robolectric.shadows.ShadowLooper;
 import org.robolectric.shadows.ShadowPackageManager;
 import org.robolectric.shadows.ShadowQueuedWork;
 
+// TODO: Don't do backup for full-backup
+// TODO: Don't do backup for stopped
+// TODO: Don't do backup for non-eligible
+// TODO: (performBackup() => SUCCESS, finishBackup() => SUCCESS) => delete stage file, renames
+// state file
+// TODO: Check agent writes state file => check file content
+// TODO: Check agent writes new state file => next agent reads it correctly
+// TODO: Check non-incremental has empty state file
+// TODO: Check queue of 2, transport rejecting package but other package proceeds
+// TODO: Check queue in general, behavior w/ multiple packages
+// TODO: Check quota is passed from transport to agent
+// TODO: Check non-incremental and transport requests PM in queue
+// TODO: Verify initialization
 @RunWith(FrameworkRobolectricTestRunner.class)
 @Config(
         manifest = Config.NONE,
@@ -114,20 +143,22 @@ public class PerformBackupTaskTest {
     private static final String PACKAGE_1 = "com.example.package1";
     private static final String PACKAGE_2 = "com.example.package2";
 
-    @Mock private BackupManagerService mBackupManagerService;
     @Mock private TransportManager mTransportManager;
     @Mock private DataChangedJournal mDataChangedJournal;
     @Mock private IBackupObserver mObserver;
     @Mock private IBackupManagerMonitor mMonitor;
     @Mock private OnTaskFinishedListener mListener;
+    private BackupManagerService mBackupManagerService;
     private TransportData mTransport;
     private ShadowLooper mShadowBackupLooper;
-    private BackupHandler mBackupHandler;
+    private Handler mBackupHandler;
     private PowerManager.WakeLock mWakeLock;
     private ShadowPackageManager mShadowPackageManager;
     private FakeIBackupManager mBackupManager;
     private File mBaseStateDir;
+    private File mDataDir;
     private Application mApplication;
+    private Context mContext;
 
     @Before
     public void setUp() throws Exception {
@@ -136,49 +167,269 @@ public class PerformBackupTaskTest {
         mTransport = backupTransport();
 
         mApplication = RuntimeEnvironment.application;
+        mContext = mApplication;
+
         File cacheDir = mApplication.getCacheDir();
-        mBaseStateDir = new File(cacheDir, "base_state_dir");
-        File dataDir = new File(cacheDir, "data_dir");
-        assertThat(mBaseStateDir.mkdir()).isTrue();
-        assertThat(dataDir.mkdir()).isTrue();
+        // Corresponds to /data/backup
+        mBaseStateDir = new File(cacheDir, "base_state");
+        // Corresponds to /cache/backup_stage
+        mDataDir = new File(cacheDir, "data");
+        // We create here simulating init.rc
+        mDataDir.mkdirs();
+        assertThat(mDataDir.isDirectory()).isTrue();
 
         PackageManager packageManager = mApplication.getPackageManager();
         mShadowPackageManager = shadowOf(packageManager);
 
         mWakeLock = createBackupWakeLock(mApplication);
 
-        Looper backupLooper = startBackupThreadAndGetLooper();
-        mShadowBackupLooper = shadowOf(backupLooper);
-
-        Handler mainHandler = new Handler(Looper.getMainLooper());
-        BackupAgentTimeoutParameters agentTimeoutParameters =
-                new BackupAgentTimeoutParameters(mainHandler, mApplication.getContentResolver());
-        agentTimeoutParameters.start();
-
-        // We need to mock BMS timeout parameters before initializing the BackupHandler since
-        // the constructor of BackupHandler relies on the timeout parameters.
-        when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
-        mBackupHandler = new BackupHandler(mBackupManagerService, backupLooper);
-
         mBackupManager = spy(FakeIBackupManager.class);
 
-        BackupManagerConstants constants =
-                new BackupManagerConstants(mainHandler, mApplication.getContentResolver());
-        constants.start();
-
+        mBackupManagerService =
+                spy(
+                        createInitializedBackupManagerService(
+                                mContext, mBaseStateDir, mDataDir, mTransportManager));
         setUpBackupManagerServiceBasics(
                 mBackupManagerService,
                 mApplication,
                 mTransportManager,
                 packageManager,
-                mBackupHandler,
+                mBackupManagerService.getBackupHandler(),
                 mWakeLock,
-                agentTimeoutParameters);
+                mBackupManagerService.getAgentTimeoutParameters());
         when(mBackupManagerService.getBaseStateDir()).thenReturn(mBaseStateDir);
-        when(mBackupManagerService.getDataDir()).thenReturn(dataDir);
+        when(mBackupManagerService.getDataDir()).thenReturn(mDataDir);
         when(mBackupManagerService.getBackupManagerBinder()).thenReturn(mBackupManager);
-        when(mBackupManagerService.getConstants()).thenReturn(constants);
-        when(mBackupManagerService.makeMetadataAgent()).thenAnswer(invocation -> createPmAgent());
+
+        mBackupHandler = mBackupManagerService.getBackupHandler();
+        mShadowBackupLooper = shadowOf(mBackupHandler.getLooper());
+    }
+
+    @Test
+    public void testRunTask_whenQueueEmpty() throws Exception {
+        when(mBackupManagerService.getCurrentToken()).thenReturn(0L);
+        TransportMock transportMock = setUpTransport(mTransport);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, true);
+
+        runTask(task);
+
+        assertThat(mBackupManagerService.getPendingInits()).isEmpty();
+        assertThat(mBackupManagerService.isBackupRunning()).isFalse();
+        assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0);
+        assertThat(mWakeLock.isHeld()).isFalse();
+        assertDirectory(getStateDirectory(mTransport)).isEmpty();
+        assertDirectory(mDataDir.toPath()).isEmpty();
+        verify(transportMock.transport, never()).initializeDevice();
+        verify(transportMock.transport, never()).performBackup(any(), any(), anyInt());
+        verify(transportMock.transport, never()).finishBackup();
+        verify(mDataChangedJournal).delete();
+        verify(mListener).onFinished(any());
+        verify(mObserver, never()).onResult(any(), anyInt());
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+        // TODO: Verify set current token?
+    }
+
+    @Test
+    public void testRunTask_whenQueueEmpty_doesNotChangeStateFiles() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, true);
+        createPmStateFile();
+        Files.write(getStateFile(mTransport, PACKAGE_1), "packageState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_MANAGER_SENTINEL))).isEqualTo("pmState".getBytes());
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))).isEqualTo("packageState".getBytes());
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackage_aboutAgent() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                });
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+
+        runTask(task);
+
+        verify(agentMock.agent).onBackup(any(), any(), any());
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("newState".getBytes());
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackage_notifiesCorrectly() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).logBackupComplete(PACKAGE_1);
+        verify(mObserver).onResult(PACKAGE_1, BackupManager.SUCCESS);
+        verify(mListener).onFinished(any());
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackage_releasesWakeLock() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(mWakeLock.isHeld()).isFalse();
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackage_updatesBookkeeping() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(mBackupManagerService.getPendingInits()).isEmpty();
+        assertThat(mBackupManagerService.isBackupRunning()).isFalse();
+        assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0);
+        verify(mDataChangedJournal).delete();
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageIncremental_passesOldStateToAgent() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient,
+                        mTransport.transportDirName,
+                        false,
+                        PACKAGE_1);
+        createPmStateFile();
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(agentMock.oldState).isEqualTo("oldState".getBytes());
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageNonIncremental_passesEmptyOldStateToAgent() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient,
+                        mTransport.transportDirName,
+                        true,
+                        PACKAGE_1);
+        createPmStateFile();
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(agentMock.oldState).isEqualTo(new byte[0]);
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageNonIncremental_doesNotBackUpPm() throws Exception {
+        PackageManagerBackupAgent pmAgent = spy(createPmAgent());
+        when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent));
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient,
+                        mTransport.transportDirName,
+                        true,
+                        PACKAGE_1);
+
+        runTask(task);
+
+        verify(pmAgent, never()).onBackup(any(), any(), any());
+    }
+
+    @Test
+    public void testRunTask_whenPackageAndPmNonIncremental_backsUpPm() throws Exception {
+        PackageManagerBackupAgent pmAgent = spy(createPmAgent());
+        when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent));
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient,
+                        mTransport.transportDirName,
+                        true,
+                        PACKAGE_1,
+                        PACKAGE_MANAGER_SENTINEL);
+
+        runTask(task);
+
+        verify(pmAgent).onBackup(any(), any(), any());
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageIncremental_backsUpPm() throws Exception {
+        PackageManagerBackupAgent pmAgent = spy(createPmAgent());
+        when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent));
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient,
+                        mTransport.transportDirName,
+                        false,
+                        PACKAGE_1);
+
+        runTask(task);
+
+        verify(pmAgent).onBackup(any(), any(), any());
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageNoPmState_initializesTransport() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+        Files.deleteIfExists(getStateFile(mTransport, PACKAGE_MANAGER_SENTINEL));
+
+        runTask(task);
+
+        verify(transportMock.transport).initializeDevice();
+    }
+
+    @Test
+    public void testRunTask_whenSinglePackageWithPmState_doesNotInitializeTransport()
+            throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        PerformBackupTask task =
+                createPerformBackupTask(
+                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
+        createPmStateFile();
+
+        runTask(task);
+
+        verify(transportMock.transport, never()).initializeDevice();
     }
 
     @Test
@@ -249,20 +500,6 @@ public class PerformBackupTaskTest {
     }
 
     @Test
-    public void testRunTask_callsListenerAndObserver() throws Exception {
-        TransportMock transportMock = setUpTransport(mTransport);
-        setUpAgent(PACKAGE_1);
-        PerformBackupTask task =
-                createPerformBackupTask(
-                        transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
-
-        runTask(task);
-
-        verify(mListener).onFinished(any());
-        verify(mObserver).backupFinished(eq(BackupManager.SUCCESS));
-    }
-
-    @Test
     public void testRunTask_releasesWakeLock() throws Exception {
         TransportMock transportMock = setUpTransport(mTransport);
         setUpAgent(PACKAGE_1);
@@ -281,7 +518,7 @@ public class PerformBackupTaskTest {
         IBackupTransport transportBinder = transportMock.transport;
         AgentMock agentMock = setUpAgent(PACKAGE_1);
         agentOnBackupDo(
-                agentMock.agent,
+                agentMock,
                 (oldState, dataOutput, newState) -> {
                     writeData(dataOutput, "key1", "foo".getBytes());
                     writeData(dataOutput, "key2", "bar".getBytes());
@@ -289,43 +526,48 @@ public class PerformBackupTaskTest {
         PerformBackupTask task =
                 createPerformBackupTask(
                         transportMock.transportClient, mTransport.transportDirName, PACKAGE_1);
-        // We need to verify at call time because the file is deleted right after
+        Path backupDataPath =
+                Files.createTempFile(mContext.getCacheDir().toPath(), "backup", ".tmp");
         when(transportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
-                .then(this::mockAndVerifyTransportPerformBackupData);
+                .then(
+                        invocation -> {
+                            ParcelFileDescriptor backupDataParcelFd = invocation.getArgument(1);
+                            FileDescriptor backupDataFd = backupDataParcelFd.getFileDescriptor();
+                            Files.copy(
+                                    new FileInputStream(backupDataFd),
+                                    backupDataPath,
+                                    REPLACE_EXISTING);
+                            backupDataParcelFd.close();
+                            return BackupTransport.TRANSPORT_OK;
+                        });
 
         runTask(task);
 
-        // Already verified data in mockAndVerifyPerformBackupData
         verify(transportBinder).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());
+        // Now verify data sent
+        FileInputStream inputStream = new FileInputStream(backupDataPath.toFile());
+        BackupDataInput backupData = new BackupDataInput(inputStream.getFD());
 
         // "key1" => "foo"
-        assertThat(dataInput.readNextHeader()).isTrue();
-        assertThat(dataInput.getKey()).isEqualTo("key1");
-        int size1 = dataInput.getDataSize();
+        assertThat(backupData.readNextHeader()).isTrue();
+        assertThat(backupData.getKey()).isEqualTo("key1");
+        int size1 = backupData.getDataSize();
         byte[] data1 = new byte[size1];
-        dataInput.readEntityData(data1, 0, size1);
+        backupData.readEntityData(data1, 0, size1);
         assertThat(data1).isEqualTo("foo".getBytes());
 
         // "key2" => "bar"
-        assertThat(dataInput.readNextHeader()).isTrue();
-        assertThat(dataInput.getKey()).isEqualTo("key2");
-        int size2 = dataInput.getDataSize();
+        assertThat(backupData.readNextHeader()).isTrue();
+        assertThat(backupData.getKey()).isEqualTo("key2");
+        int size2 = backupData.getDataSize();
         byte[] data2 = new byte[size2];
-        dataInput.readEntityData(data2, 0, size2);
+        backupData.readEntityData(data2, 0, size2);
         assertThat(data2).isEqualTo("bar".getBytes());
 
         // No more
-        assertThat(dataInput.readNextHeader()).isFalse();
-
-        return BackupTransport.TRANSPORT_OK;
+        assertThat(backupData.readNextHeader()).isFalse();
+        inputStream.close();
     }
 
     @Test
@@ -350,7 +592,7 @@ public class PerformBackupTaskTest {
         TransportMock transportMock = setUpTransport(mTransport);
         AgentMock agentMock = setUpAgent(PACKAGE_1);
         agentOnBackupDo(
-                agentMock.agent,
+                agentMock,
                 (oldState, dataOutput, newState) -> {
                     char prohibitedChar = 0xff00;
                     writeData(dataOutput, prohibitedChar + "key", "foo".getBytes());
@@ -374,13 +616,13 @@ public class PerformBackupTaskTest {
         AgentMock agentMock1 = agentMocks.get(0);
         AgentMock agentMock2 = agentMocks.get(1);
         agentOnBackupDo(
-                agentMock1.agent,
+                agentMock1,
                 (oldState, dataOutput, newState) -> {
                     char prohibitedChar = 0xff00;
                     writeData(dataOutput, prohibitedChar + "key", "foo".getBytes());
                 });
         agentOnBackupDo(
-                agentMock2.agent,
+                agentMock2,
                 (oldState, dataOutput, newState) -> {
                     writeData(dataOutput, "key", "bar".getBytes());
                 });
@@ -528,6 +770,7 @@ public class PerformBackupTaskTest {
 
         runTask(task);
 
+        // Error because it was non-incremental already, so transport can't request it
         verify(mObserver).onResult(PACKAGE_1, BackupManager.ERROR_TRANSPORT_ABORTED);
         verify(mObserver).backupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
     }
@@ -553,10 +796,9 @@ public class PerformBackupTaskTest {
                         mTransport.transportDirName,
                         false,
                         PACKAGE_1);
-        // Write content to be incremental
-        Files.write(
-                Paths.get(mBaseStateDir.getAbsolutePath(), mTransport.transportDirName, PACKAGE_1),
-                "existent".getBytes());
+        createPmStateFile();
+        // Write state to be incremental
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
 
         runTask(task);
 
@@ -566,25 +808,12 @@ public class PerformBackupTaskTest {
     }
 
     @Test
-    public void testRunTask_whenQueueEmpty() throws Exception {
-        TransportMock transportMock = setUpTransport(mTransport);
-        PerformBackupTask task =
-                createPerformBackupTask(
-                        transportMock.transportClient, mTransport.transportDirName, true);
-
-        runTask(task);
-
-        verify(mObserver).backupFinished(eq(BackupManager.SUCCESS));
-    }
-
-    @Test
     public void testRunTask_whenIncrementalAndTransportUnavailableDuringPmBackup()
             throws Exception {
         TransportMock transportMock = setUpTransport(mTransport);
         IBackupTransport transportBinder = transportMock.transport;
         setUpAgent(PACKAGE_1);
-        when(transportBinder.getBackupQuota(
-                        eq(BackupManagerService.PACKAGE_MANAGER_SENTINEL), anyBoolean()))
+        when(transportBinder.getBackupQuota(eq(PACKAGE_MANAGER_SENTINEL), anyBoolean()))
                 .thenThrow(DeadObjectException.class);
         PerformBackupTask task =
                 createPerformBackupTask(
@@ -600,9 +829,10 @@ public class PerformBackupTaskTest {
     }
 
     @Test
-    public void testRunTask_whenIncrementalAndAgentFails() throws Exception {
+    public void testRunTask_whenIncrementalAndPmAgentFails() throws Exception {
         TransportMock transportMock = setUpTransport(mTransport);
-        createFakePmAgent();
+        PackageManagerBackupAgent pmAgent = createThrowingPmAgent();
+        when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent);
         PerformBackupTask task =
                 createPerformBackupTask(
                         transportMock.transportClient,
@@ -627,11 +857,18 @@ public class PerformBackupTaskTest {
     private TransportMock setUpTransport(TransportData transport) throws Exception {
         TransportMock transportMock =
                 TransportTestUtils.setUpTransport(mTransportManager, transport);
-        File stateDir = new File(mBaseStateDir, transport.transportDirName);
-        assertThat(stateDir.mkdir()).isTrue();
+        Files.createDirectories(getStateDirectory(transport));
         return transportMock;
     }
 
+    private Path getStateDirectory(TransportData transport) {
+        return mBaseStateDir.toPath().resolve(transport.transportDirName);
+    }
+
+    private Path getStateFile(TransportData transport, String packageName) {
+        return getStateDirectory(transport).resolve(packageName);
+    }
+
     private List<AgentMock> setUpAgents(String... packageNames) {
         return Stream.of(packageNames).map(this::setUpAgent).collect(toList());
     }
@@ -652,9 +889,9 @@ public class PerformBackupTaskTest {
                     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);
+            doReturn(backupAgentBinder)
+                    .when(mBackupManagerService)
+                    .bindToAgentSynchronous(eq(packageInfo.applicationInfo), anyInt());
             return new AgentMock(backupAgentBinder, backupAgent);
         } catch (RemoteException e) {
             // Never happens, compiler happy
@@ -668,12 +905,15 @@ public class PerformBackupTaskTest {
 
     private AgentMock setUpAgentWithData(String packageName) {
         AgentMock agentMock = setUpAgent(packageName);
+
         uncheck(
                 () ->
                         agentOnBackupDo(
-                                agentMock.agent,
-                                (oldState, dataOutput, newState) ->
-                                        writeData(dataOutput, "key", packageName.getBytes())));
+                                agentMock,
+                                (oldState, dataOutput, newState) -> {
+                                    writeData(dataOutput, "key", ("data" + packageName).getBytes());
+                                    writeState(newState, ("state" + packageName).getBytes());
+                                }));
         return agentMock;
     }
 
@@ -719,13 +959,12 @@ public class PerformBackupTaskTest {
      * Returns an implementation of PackageManagerBackupAgent that throws RuntimeException in {@link
      * BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)}
      */
-    private PackageManagerBackupAgent createFakePmAgent() {
-        PackageManagerBackupAgent fakePmAgent =
-                new FakePackageManagerBackupAgent(mApplication.getPackageManager());
-        fakePmAgent.attach(mApplication);
-        fakePmAgent.onCreate();
-        when(mBackupManagerService.makeMetadataAgent()).thenReturn(fakePmAgent);
-        return fakePmAgent;
+    private PackageManagerBackupAgent createThrowingPmAgent() {
+        PackageManagerBackupAgent pmAgent =
+                new ThrowingPackageManagerBackupAgent(mApplication.getPackageManager());
+        pmAgent.attach(mApplication);
+        pmAgent.onCreate();
+        return pmAgent;
     }
 
     /** Matches {@link PackageInfo} whose package name is {@code packageName}. */
@@ -751,9 +990,49 @@ public class PerformBackupTaskTest {
         dataOutput.writeEntityData(data, data.length);
     }
 
-    private static void agentOnBackupDo(BackupAgent agent, BackupAgentOnBackup function)
+    private static void writeState(ParcelFileDescriptor newState, byte[] state) throws IOException {
+        OutputStream outputStream = new FileOutputStream(newState.getFileDescriptor());
+        outputStream.write(state);
+        outputStream.flush();
+    }
+
+    /** Prevents the states from being reset and transport initialization. */
+    private void createPmStateFile() throws IOException {
+        Files.write(getStateFile(mTransport, PACKAGE_MANAGER_SENTINEL), "pmState".getBytes());
+    }
+
+    /**
+     * Implements {@code function} for {@link BackupAgent#onBackup(ParcelFileDescriptor,
+     * BackupDataOutput, ParcelFileDescriptor)} of {@code agentMock} and populates {@link
+     * AgentMock#oldState}.
+     */
+    private static void agentOnBackupDo(AgentMock agentMock, BackupAgentOnBackup function)
             throws Exception {
-        doAnswer(function).when(agent).onBackup(any(), any(), any());
+        doAnswer(
+                        (BackupAgentOnBackup)
+                                (oldState, dataOutput, newState) -> {
+                                    ByteArrayOutputStream outputStream =
+                                            new ByteArrayOutputStream();
+                                    Utils.transferStreamedData(
+                                            new FileInputStream(oldState.getFileDescriptor()),
+                                            outputStream);
+                                    agentMock.oldState = outputStream.toByteArray();
+                                    function.onBackup(oldState, dataOutput, newState);
+                                })
+                .when(agentMock.agent)
+                .onBackup(any(), any(), any());
+    }
+
+    // TODO: Find some implementation? Extract?
+    private static <T> Iterable<T> oneTimeIterable(Iterator<T> iterator) {
+        return () -> iterator;
+    }
+
+    private static IterableSubject<
+                    ? extends IterableSubject<?, Path, Iterable<Path>>, Path, Iterable<Path>>
+            assertDirectory(Path directory) throws IOException {
+        return assertThat(oneTimeIterable(Files.newDirectoryStream(directory).iterator()))
+                .named("directory " + directory);
     }
 
     @FunctionalInterface
@@ -777,6 +1056,7 @@ public class PerformBackupTaskTest {
     private static class AgentMock {
         private final IBackupAgent agentBinder;
         private final BackupAgent agent;
+        private byte[] oldState;
 
         private AgentMock(IBackupAgent agentBinder, BackupAgent agent) {
             this.agentBinder = agentBinder;
@@ -805,8 +1085,8 @@ public class PerformBackupTaskTest {
         }
     }
 
-    private static class FakePackageManagerBackupAgent extends PackageManagerBackupAgent {
-        public FakePackageManagerBackupAgent(PackageManager packageMgr) {
+    private static class ThrowingPackageManagerBackupAgent extends PackageManagerBackupAgent {
+        ThrowingPackageManagerBackupAgent(PackageManager packageMgr) {
             super(packageMgr);
         }
 
diff --git a/services/robotests/src/com/android/server/backup/Utils.java b/services/robotests/src/com/android/server/backup/Utils.java
new file mode 100644 (file)
index 0000000..7cdca17
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.backup;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Utils {
+    public static final int BUFFER_SIZE = 8192;
+
+    public static void transferStreamedData(InputStream in, OutputStream out) throws IOException {
+        transferStreamedData(in, out, BUFFER_SIZE);
+    }
+
+    public static void transferStreamedData(InputStream in, OutputStream out, int bufferSize)
+            throws IOException {
+        byte[] buffer = new byte[bufferSize];
+        int read;
+        while ((read = in.read(buffer)) != -1) {
+            out.write(buffer, 0, read);
+        }
+    }
+
+    private Utils() {}
+}
index 92d6bbd..c51b75b 100644 (file)
@@ -77,10 +77,9 @@ import java.util.ArrayDeque;
 
 @RunWith(FrameworkRobolectricTestRunner.class)
 @Config(
-    manifest = Config.NONE,
-    sdk = 26,
-    shadows = {ShadowEventLog.class, ShadowPerformUnifiedRestoreTask.class, ShadowBinder.class}
-)
+        manifest = Config.NONE,
+        sdk = 26,
+        shadows = {ShadowEventLog.class, ShadowPerformUnifiedRestoreTask.class, ShadowBinder.class})
 @SystemLoaderPackages({"com.android.server.backup"})
 @Presubmit
 public class ActiveRestoreSessionTest {
@@ -130,6 +129,7 @@ public class ActiveRestoreSessionTest {
 
         mWakeLock = createBackupWakeLock(application);
 
+        // TODO: Migrate to use spy(createInitializedBackupManagerService())
         setUpBackupManagerServiceBasics(
                 mBackupManagerService,
                 application,
index 5a886e3..522578f 100644 (file)
 
 package com.android.server.backup.testing;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
 
+import android.annotation.Nullable;
 import android.app.Application;
 import android.app.IActivityManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.PowerManager;
@@ -30,32 +37,118 @@ import android.util.SparseArray;
 
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupManagerService;
+import com.android.server.backup.Trampoline;
 import com.android.server.backup.TransportManager;
-import com.android.server.backup.internal.BackupHandler;
+import com.android.server.backup.internal.Operation;
 
+import org.mockito.stubbing.Answer;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.io.File;
 import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.concurrent.atomic.AtomicReference;
 
 /** Test utils for {@link BackupManagerService} and friends. */
 public class BackupManagerServiceTestUtils {
-    /** Sets up basic mocks for {@link BackupManagerService}. */
+    public static BackupManagerService createInitializedBackupManagerService(
+            Context context, File baseStateDir, File dataDir, TransportManager transportManager) {
+        return createInitializedBackupManagerService(
+                context,
+                startBackupThread(null),
+                baseStateDir,
+                dataDir,
+                transportManager);
+    }
+
+    public static BackupManagerService createInitializedBackupManagerService(
+            Context context,
+            HandlerThread backupThread,
+            File baseStateDir,
+            File dataDir,
+            TransportManager transportManager) {
+        BackupManagerService backupManagerService =
+                new BackupManagerService(
+                        context,
+                        new Trampoline(context),
+                        backupThread,
+                        baseStateDir,
+                        dataDir,
+                        transportManager);
+        ShadowLooper shadowBackupLooper = shadowOf(backupThread.getLooper());
+        shadowBackupLooper.runToEndOfTasks();
+        // Handler instances have their own clock, so advancing looper (with runToEndOfTasks())
+        // above does NOT advance the handlers' clock, hence whenever a handler post messages with
+        // specific time to the looper the time of those messages will be before the looper's time.
+        // To fix this we advance SystemClock as well since that is from where the handlers read
+        // time.
+        ShadowSystemClock.setCurrentTimeMillis(shadowBackupLooper.getScheduler().getCurrentTime());
+        return backupManagerService;
+    }
+
+    /** Sets up basic mocks for {@link BackupManagerService} mock. */
+    @SuppressWarnings("ResultOfMethodCallIgnored")
     public static void setUpBackupManagerServiceBasics(
             BackupManagerService backupManagerService,
             Context context,
             TransportManager transportManager,
             PackageManager packageManager,
-            BackupHandler backupHandler,
+            Handler backupHandler,
             PowerManager.WakeLock wakeLock,
             BackupAgentTimeoutParameters agentTimeoutParameters) {
+        SparseArray<Operation> operations = new SparseArray<>();
+
         when(backupManagerService.getContext()).thenReturn(context);
         when(backupManagerService.getTransportManager()).thenReturn(transportManager);
         when(backupManagerService.getPackageManager()).thenReturn(packageManager);
         when(backupManagerService.getBackupHandler()).thenReturn(backupHandler);
         when(backupManagerService.getCurrentOpLock()).thenReturn(new Object());
         when(backupManagerService.getQueueLock()).thenReturn(new Object());
-        when(backupManagerService.getCurrentOperations()).thenReturn(new SparseArray<>());
+        when(backupManagerService.getCurrentOperations()).thenReturn(operations);
         when(backupManagerService.getActivityManager()).thenReturn(mock(IActivityManager.class));
         when(backupManagerService.getWakelock()).thenReturn(wakeLock);
         when(backupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
+
+        AccessorMock backupEnabled = mockAccessor(false);
+        doAnswer(backupEnabled.getter).when(backupManagerService).isBackupEnabled();
+        doAnswer(backupEnabled.setter).when(backupManagerService).setBackupEnabled(anyBoolean());
+
+        AccessorMock backupRunning = mockAccessor(false);
+        doAnswer(backupEnabled.getter).when(backupManagerService).isBackupRunning();
+        doAnswer(backupRunning.setter).when(backupManagerService).setBackupRunning(anyBoolean());
+
+        doAnswer(
+                        invocation -> {
+                            operations.put(invocation.getArgument(0), invocation.getArgument(1));
+                            return null;
+                        })
+                .when(backupManagerService)
+                .putOperation(anyInt(), any());
+        doAnswer(
+                        invocation -> {
+                            int token = invocation.getArgument(0);
+                            operations.remove(token);
+                            return null;
+                        })
+                .when(backupManagerService)
+                .removeOperation(anyInt());
+    }
+
+    /**
+     * Returns one getter {@link Answer<T>} and one setter {@link Answer<T>} to be easily passed to
+     * Mockito mocking facilities.
+     *
+     * @param defaultValue Value returned by the getter if there was no setter call until then.
+     */
+    public static <T> AccessorMock<T> mockAccessor(T defaultValue) {
+        AtomicReference<T> holder = new AtomicReference<>(defaultValue);
+        return new AccessorMock<>(
+                invocation -> holder.get(),
+                invocation -> {
+                    holder.set(invocation.getArgument(0));
+                    return null;
+                });
     }
 
     public static PowerManager.WakeLock createBackupWakeLock(Application application) {
@@ -88,12 +181,38 @@ public class BackupManagerServiceTestUtils {
      * @return The backup thread.
      * @see #startBackupThreadAndGetLooper()
      */
-    public static HandlerThread startBackupThread(UncaughtExceptionHandler exceptionHandler) {
+    public static HandlerThread startBackupThread(
+            @Nullable UncaughtExceptionHandler exceptionHandler) {
         HandlerThread backupThread = new HandlerThread("backup");
         backupThread.setUncaughtExceptionHandler(exceptionHandler);
         backupThread.start();
         return backupThread;
     }
 
+    /**
+     * Similar to {@link #startBackupThread(UncaughtExceptionHandler)} but logging uncaught
+     * exceptions to logcat.
+     *
+     * @param tag Tag used for logging exceptions.
+     * @return The backup thread.
+     * @see #startBackupThread(UncaughtExceptionHandler)
+     */
+    public static HandlerThread startSilentBackupThread(String tag) {
+        return startBackupThread(
+                (thread, e) ->
+                        ShadowLog.e(
+                                tag, "Uncaught exception in test thread " + thread.getName(), e));
+    }
+
     private BackupManagerServiceTestUtils() {}
+
+    public static class AccessorMock<T> {
+        public Answer<T> getter;
+        public Answer<T> setter;
+
+        private AccessorMock(Answer<T> getter, Answer<T> setter) {
+            this.getter = getter;
+            this.setter = setter;
+        }
+    }
 }
index 5844131..6625443 100644 (file)
@@ -26,13 +26,13 @@ import static org.mockito.Mockito.when;
 
 import static java.util.stream.Collectors.toList;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.os.RemoteException;
-import android.support.annotation.IntDef;
 
 import com.android.internal.backup.IBackupTransport;
 import com.android.server.backup.TransportManager;
@@ -42,6 +42,8 @@ import com.android.server.backup.transport.TransportNotRegisteredException;
 
 import org.robolectric.shadows.ShadowPackageManager;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.stream.Stream;
 
@@ -209,6 +211,7 @@ public class TransportTestUtils {
         TransportStatus.REGISTERED_UNAVAILABLE,
         TransportStatus.UNREGISTERED
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface TransportStatus {
         int REGISTERED_AVAILABLE = 0;
         int REGISTERED_UNAVAILABLE = 1;