Tests for missing blocks.

+improve the streaming framework to work with failed installations

Bug: 160635126
Test: atest PackageManagerShellCommandIncrementalTest#testInstallWithMissingBlocks --iterations

Change-Id: If5e97307a2fb43fdac9725ce98d77d589ff5e138
diff --git a/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java b/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java
index a4b6cd7..ea96139 100644
--- a/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java
+++ b/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java
@@ -32,6 +32,7 @@
 import android.system.Os;
 import android.system.OsConstants;
 import android.system.StructPollfd;
+import android.util.Log;
 import android.util.Slog;
 
 import com.android.incfs.install.IDeviceConnection;
@@ -41,17 +42,18 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 
 public class IncrementalDeviceConnection implements IDeviceConnection {
     private static final String TAG = "IncrementalDeviceConnection";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     private static final int POLL_TIMEOUT_MS = 300000;
 
-    enum ConnectionType {
+    private enum ConnectionType {
         RELIABLE,
         UNRELIABLE,
     }
@@ -125,8 +127,8 @@
 
     @Override
     public void close() throws Exception {
-        IoUtils.closeQuietly(mPfd);
         mShellCommand.join();
+        IoUtils.closeQuietly(mPfd);
     }
 
     static class Logger implements ILogger {
@@ -156,9 +158,23 @@
 
     static class Factory implements IDeviceConnection.Factory {
         private final ConnectionType mConnectionType;
+        private final boolean mExpectInstallationSuccess;
 
-        Factory(ConnectionType connectionType) {
+        static Factory reliable() {
+            return new Factory(ConnectionType.RELIABLE, true);
+        }
+
+        static Factory ureliable() {
+            return new Factory(ConnectionType.UNRELIABLE, false);
+        }
+
+        static Factory reliableExpectInstallationFailure() {
+            return new Factory(ConnectionType.RELIABLE, false);
+        }
+
+        private Factory(ConnectionType connectionType, boolean expectInstallationSuccess) {
             mConnectionType = connectionType;
+            mExpectInstallationSuccess = expectInstallationSuccess;
         }
 
         @Override
@@ -172,31 +188,29 @@
             final ParcelFileDescriptor processPfd = pipe[1];
 
             final ResultReceiver resultReceiver;
-            if (mConnectionType == ConnectionType.RELIABLE) {
+            if (mExpectInstallationSuccess) {
                 resultReceiver = new ResultReceiver(null) {
                     @Override
                     protected void onReceiveResult(int resultCode, Bundle resultData) {
                         if (resultCode == 0) {
                             return;
                         }
-                        try {
-                            final String message = readFullStream(
-                                    new ParcelFileDescriptor.AutoCloseInputStream(localPfd));
-                            assertEquals(message, 0, resultCode);
-                        } catch (IOException e) {
-                            assertNull("Failed to pull error from failed command: " + resultCode
-                                    + ", exception: " + e, e);
-                        }
+                        final String message = readFullStreamOrError(
+                                new FileInputStream(localPfd.getFileDescriptor()));
+                        assertEquals(message, 0, resultCode);
                     }
                 };
             } else {
                 resultReceiver = new ResultReceiver(null) {
                     @Override
                     protected void onReceiveResult(int resultCode, Bundle resultData) {
-                        try {
-                            readFullStream(new ParcelFileDescriptor.AutoCloseInputStream(localPfd));
-                        } catch (IOException ignored) {
+                        if (resultCode == 0) {
+                            return;
                         }
+                        final String message = readFullStreamOrError(
+                                new FileInputStream(localPfd.getFileDescriptor()));
+                        Log.i(TAG, "Installation finished with code: " + resultCode + ", message: "
+                                + message);
                     }
                 };
             }
@@ -218,14 +232,20 @@
         }
     }
 
-    private static String readFullStream(InputStream inputStream) throws IOException {
+    private static String readFullStreamOrError(InputStream inputStream) {
         try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
-            final byte[] buffer = new byte[1024];
-            int length;
-            while ((length = inputStream.read(buffer)) != -1) {
-                result.write(buffer, 0, length);
+            try {
+                final byte[] buffer = new byte[1024];
+                int length;
+                while ((length = inputStream.read(buffer)) != -1) {
+                    result.write(buffer, 0, length);
+                }
+            } catch (IOException e) {
+                return result.toString("UTF-8") + " exception [" + e + "]";
             }
             return result.toString("UTF-8");
+        } catch (IOException e) {
+            return e.toString();
         }
     }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
index 240d0be..1440aeb 100644
--- a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
@@ -40,11 +40,13 @@
 import android.service.dataloader.DataLoaderService;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.incfs.install.IBlockFilter;
 import com.android.incfs.install.IBlockTransformer;
 import com.android.incfs.install.IncrementalInstallSession;
 import com.android.incfs.install.PendingBlock;
@@ -72,6 +74,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.Random;
 import java.util.Scanner;
 import java.util.UUID;
 import java.util.concurrent.Callable;
@@ -88,6 +91,8 @@
 @AppModeFull
 @LargeTest
 public class PackageManagerShellCommandIncrementalTest {
+    private static final String TAG = "PackageManagerShellCommandIncrementalTest";
+
     private static final String CTS_PACKAGE_NAME = "android.content.cts";
     private static final String TEST_APP_PACKAGE = "com.example.helloworld";
 
@@ -202,6 +207,7 @@
                 executeShellCommand("pm install-incremental -t -g " + file.getPath()));
     }
 
+    @LargeTest
     @Test
     public void testInstallWithIdSigAndSplit() throws Exception {
         File apkfile = new File(createApkPath(TEST_APK));
@@ -232,10 +238,8 @@
                         .build();
         getUiAutomation().adoptShellPermissionIdentity();
         try {
-            mSession.start(
-                    Executors.newSingleThreadExecutor(),
-                    new IncrementalDeviceConnection.Factory(
-                            IncrementalDeviceConnection.ConnectionType.RELIABLE));
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliable());
             mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
         } finally {
             getUiAutomation().dropShellPermissionIdentity();
@@ -243,6 +247,65 @@
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
     }
 
+    @LargeTest
+    @Test
+    public void testInstallWithMissingBlocks() throws Exception {
+        setDeviceProperty("incfs_default_timeouts", "0:0:0");
+        setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME);
+        setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
+                "0");
+
+        final long randomSeed = System.currentTimeMillis();
+        Log.i(TAG, "Randomizing missing blocks with seed: " + randomSeed);
+        final Random random = new Random(randomSeed);
+
+        // TODO: add detection of orphaned IncFS instances after failed installations
+
+        final int blockSize = 4096;
+        final int retries = 7; // 7 * 3s + leeway ~= 30secs of test timeout
+
+        final File apk = new File(createApkPath(TEST_APK));
+        final int blocks = (int) (apk.length() / blockSize);
+
+        for (int i = 0; i < retries; ++i) {
+            final int skipBlock = random.nextInt(blocks);
+            Log.i(TAG, "skipBlock: " + skipBlock + " out of " + blocks);
+            try {
+                installWithBlockFilter((block -> block.getType() == PendingBlock.Type.SIGNATURE_TREE
+                        || block.getBlockIndex() != skipBlock));
+                if (isAppInstalled(TEST_APP_PACKAGE)) {
+                    uninstallPackageSilently(TEST_APP_PACKAGE);
+                }
+            } catch (RuntimeException re) {
+                Log.i(TAG, "RuntimeException: ", re);
+                assertTrue(re.toString(), re.getCause() instanceof IOException);
+            } catch (IOException e) {
+                Log.i(TAG, "IOException: ", e);
+                throw e;
+            }
+        }
+    }
+
+    public void installWithBlockFilter(IBlockFilter blockFilter) throws Exception {
+        final String apk = createApkPath(TEST_APK);
+        final String idsig = createApkPath(TEST_APK_IDSIG);
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(apk), Paths.get(idsig))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .setBlockFilter(blockFilter)
+                        .build();
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliableExpectInstallationFailure());
+            mSession.waitForAnyCompletion(3, TimeUnit.SECONDS);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
     /**
      * Compress the data if the compressed size is < original size, otherwise return the original
      * data.
@@ -322,10 +385,8 @@
                         .build();
         getUiAutomation().adoptShellPermissionIdentity();
         try {
-            mSession.start(
-                    Executors.newSingleThreadExecutor(),
-                    new IncrementalDeviceConnection.Factory(
-                            IncrementalDeviceConnection.ConnectionType.RELIABLE));
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliable());
             mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
         } finally {
             getUiAutomation().dropShellPermissionIdentity();
@@ -346,10 +407,8 @@
                         .build();
         getUiAutomation().adoptShellPermissionIdentity();
         try {
-            mSession.start(
-                    Executors.newSingleThreadExecutor(),
-                    new IncrementalDeviceConnection.Factory(
-                            IncrementalDeviceConnection.ConnectionType.UNRELIABLE));
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.ureliable());
             mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
         } catch (Exception ignored) {
             // Ignore, we are looking for crashes anyway.
@@ -730,10 +789,8 @@
             // Partially install the apk+split0+split1.
             getUiAutomation().adoptShellPermissionIdentity();
             try {
-                mSession.start(
-                        Executors.newSingleThreadExecutor(),
-                        new IncrementalDeviceConnection.Factory(
-                                IncrementalDeviceConnection.ConnectionType.RELIABLE));
+                mSession.start(Executors.newSingleThreadExecutor(),
+                        IncrementalDeviceConnection.Factory.reliable());
                 mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
                 assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
             } finally {
@@ -1046,7 +1103,7 @@
         getContext().startActivity(intent);
         return () -> {
             try {
-                return fullyLoaded.get(10, TimeUnit.SECONDS);
+                return fullyLoaded.get(30, TimeUnit.SECONDS);
             } catch (TimeoutException e) {
                 return false;
             }
@@ -1131,6 +1188,8 @@
         setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders",
                 "0");
         setSystemProperty("debug.incremental.readlogs_max_interval_sec", "10000");
+        setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
+                "1");
         IoUtils.closeQuietly(mSession);
         mSession = null;
     }