Skip the f2fs check for the automotive devices in Android 13. am: a3e0d50468

Original change: https://android-review.googlesource.com/c/platform/system/gsid/+/2557090

Change-Id: Iabd3b6b74208a1cb3b91d60a4468205726a6ddeb
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/OWNERS b/OWNERS
index 09f4d71..a9d0479 100644
--- a/OWNERS
+++ b/OWNERS
@@ -2,3 +2,4 @@
 dvander@google.com
 sspatil@google.com
 elsk@google.com
+yochiang@google.com
diff --git a/aidl/android/gsi/IImageService.aidl b/aidl/android/gsi/IImageService.aidl
index 363a919..9f84e50 100644
--- a/aidl/android/gsi/IImageService.aidl
+++ b/aidl/android/gsi/IImageService.aidl
@@ -130,7 +130,12 @@
     void removeAllImages();
 
     /**
-     * Remove all images that were marked as disabled in recovery.
+     * Mark an image as disabled.
+     */
+    void disableImage(@utf8InCpp String name);
+
+    /**
+     * Remove all images that were marked as disabled.
      */
     void removeDisabledImages();
 
diff --git a/daemon.cpp b/daemon.cpp
index 2123599..33e7705 100644
--- a/daemon.cpp
+++ b/daemon.cpp
@@ -65,7 +65,7 @@
         }
     }
 
-    android::gsi::GsiService::Register();
+    android::gsi::GsiService::Register(android::gsi::kGsiServiceName);
     {
         sp<ProcessState> ps(ProcessState::self());
         ps->startThreadPool();
diff --git a/gsi_service.cpp b/gsi_service.cpp
index 3392a1d..2caa445 100644
--- a/gsi_service.cpp
+++ b/gsi_service.cpp
@@ -87,10 +87,10 @@
     progress_ = {};
 }
 
-void GsiService::Register() {
+void GsiService::Register(const std::string& name) {
     auto lazyRegistrar = LazyServiceRegistrar::getInstance();
     android::sp<GsiService> service = new GsiService();
-    auto ret = lazyRegistrar.registerService(service, kGsiServiceName);
+    auto ret = lazyRegistrar.registerService(service, name);
 
     if (ret != android::OK) {
         LOG(FATAL) << "Could not register gsi service: " << ret;
@@ -343,7 +343,7 @@
 }
 
 binder::Status GsiService::removeGsiAsync(const sp<IGsiServiceCallback>& resultCallback) {
-    bool result;
+    bool result = false;
     auto status = removeGsi(&result);
     if (!status.isOk()) {
         LOG(ERROR) << "Could not removeGsi: " << status.exceptionMessage().string();
@@ -577,6 +577,7 @@
                                    int32_t* _aidl_return) override;
     binder::Status zeroFillNewImage(const std::string& name, int64_t bytes) override;
     binder::Status removeAllImages() override;
+    binder::Status disableImage(const std::string& name) override;
     binder::Status removeDisabledImages() override;
     binder::Status getMappedImageDevice(const std::string& name, std::string* device) override;
     binder::Status isImageDisabled(const std::string& name, bool* _aidl_return) override;
@@ -740,6 +741,16 @@
     return binder::Status::ok();
 }
 
+binder::Status ImageService::disableImage(const std::string& name) {
+    if (!CheckUid()) return UidSecurityError();
+
+    std::lock_guard<std::mutex> guard(service_->lock());
+    if (!impl_->DisableImage(name)) {
+        return BinderError("Failed to disable image: " + name);
+    }
+    return binder::Status::ok();
+}
+
 binder::Status ImageService::removeDisabledImages() {
     if (!CheckUid()) return UidSecurityError();
 
diff --git a/gsi_service.h b/gsi_service.h
index c50c101..3c4c278 100644
--- a/gsi_service.h
+++ b/gsi_service.h
@@ -36,7 +36,7 @@
 
 class GsiService : public BinderService<GsiService>, public BnGsiService {
   public:
-    static void Register();
+    static void Register(const std::string& name);
 
     binder::Status openInstall(const std::string& install_dir, int* _aidl_return) override;
     binder::Status closeInstall(int32_t* _aidl_return) override;
diff --git a/gsi_tool.cpp b/gsi_tool.cpp
index a6a79a5..181452b 100644
--- a/gsi_tool.cpp
+++ b/gsi_tool.cpp
@@ -35,6 +35,7 @@
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
 #include <android/gsi/IGsiService.h>
+#include <binder/ProcessState.h>
 #include <cutils/android_reboot.h>
 #include <libgsi/libgsi.h>
 #include <libgsi/libgsid.h>
@@ -714,6 +715,8 @@
 int main(int argc, char** argv) {
     android::base::InitLogging(argv, android::base::StderrLogger, android::base::DefaultAborter);
 
+    // Start a threadpool to service waitForService() callbacks.
+    android::ProcessState::self()->startThreadPool();
     android::sp<IGsiService> service = GetGsiService();
     if (!service) {
         return EX_SOFTWARE;
diff --git a/include/libgsi/libgsi.h b/include/libgsi/libgsi.h
index 41898df..5318341 100644
--- a/include/libgsi/libgsi.h
+++ b/include/libgsi/libgsi.h
@@ -24,8 +24,6 @@
 namespace android {
 namespace gsi {
 
-static constexpr char kGsiServiceName[] = "gsiservice";
-
 #define DSU_METADATA_PREFIX "/metadata/gsi/dsu/"
 
 // These files need to be globally readable so that fs_mgr_fstab, which is
diff --git a/include/libgsi/libgsid.h b/include/libgsi/libgsid.h
index 46f8ef5..1229ce7 100644
--- a/include/libgsi/libgsid.h
+++ b/include/libgsi/libgsid.h
@@ -21,6 +21,10 @@
 namespace android {
 namespace gsi {
 
+static constexpr char kGsiServiceName[] = "gsiservice";
+
+// Caller should start a threadpool before calling this method, otherwise waitForService() could
+// block for at least 1 second.
 android::sp<IGsiService> GetGsiService();
 
 }  // namespace gsi
diff --git a/libgsid.cpp b/libgsid.cpp
index 23eeae4..4689dcd 100644
--- a/libgsid.cpp
+++ b/libgsid.cpp
@@ -17,7 +17,7 @@
 #include <android-base/logging.h>
 #include <android/gsi/IGsiService.h>
 #include <binder/IServiceManager.h>
-#include <libgsi/libgsi.h>
+#include <libgsi/libgsid.h>
 
 namespace android {
 namespace gsi {
diff --git a/partition_installer.cpp b/partition_installer.cpp
index 5450169..ebd3b64 100644
--- a/partition_installer.cpp
+++ b/partition_installer.cpp
@@ -20,6 +20,7 @@
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
+#include <android-base/properties.h>
 #include <android-base/unique_fd.h>
 #include <ext4_utils/ext4_utils.h>
 #include <fs_mgr.h>
@@ -350,8 +351,10 @@
 
 std::optional<uint64_t> PartitionInstaller::GetMinimumFreeSpaceThreshold(
         const std::string& install_dir) {
-    // No need to retain any space if we were not installing to the internal storage.
-    if (!android::base::StartsWith(install_dir, "/data"s)) {
+    // No need to retain any space if we were not installing to the internal storage
+    // or device is not using VAB.
+    if (!android::base::StartsWith(install_dir, "/data"s)
+            || !android::base::GetBoolProperty("ro.virtual_ab.enabled", false)) {
         return 0;
     }
     // Dynamic Partitions device must have a "super" block device.
diff --git a/tests/Android.bp b/tests/Android.bp
index 5d90fd4..3035c10 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -57,10 +57,43 @@
     require_root: true,
 }
 
+java_library_host {
+    name: "DsuTestBase",
+    srcs: [
+        "DsuTestBase.java",
+    ],
+    libs: [
+        "tradefed",
+    ],
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
 java_test_host {
     name: "DSUEndtoEndTest",
     srcs: ["DSUEndtoEndTest.java"],
+    static_libs: [
+        "DsuTestBase",
+    ],
     libs: ["tradefed"],
     test_config: "dsu-test.xml",
     test_suites: ["general-tests"],
 }
+
+java_test_host {
+    name: "DsuGsiIntegrationTest",
+    srcs: [
+        "DsuGsiIntegrationTest.java",
+    ],
+    static_libs: [
+        "DsuTestBase",
+    ],
+    libs: [
+        "tradefed",
+    ],
+    test_config: "dsu_gsi_integration_test.xml",
+    test_suites: [
+        "general-tests",
+    ],
+}
diff --git a/tests/DSUEndtoEndTest.java b/tests/DSUEndtoEndTest.java
index e717079..79799bf 100644
--- a/tests/DSUEndtoEndTest.java
+++ b/tests/DSUEndtoEndTest.java
@@ -16,19 +16,14 @@
 
 package com.android.tests.dsu;
 
-import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
-import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.ZipUtil2;
 
 import org.apache.commons.compress.archivers.zip.ZipFile;
-
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
@@ -36,16 +31,13 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.IOException;
-import java.lang.Process;
-import java.lang.Runtime;
 import java.util.concurrent.TimeUnit;
 
 /**
  * Test Dynamic System Updates by booting in and out of a supplied system image
  */
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class DSUEndtoEndTest extends BaseHostJUnit4Test {
+public class DSUEndtoEndTest extends DsuTestBase {
     private static final long kDefaultUserdataSize = 4L * 1024 * 1024 * 1024;
     private static final String LPUNPACK_PATH = "bin/lpunpack";
     private static final String SIMG2IMG_PATH = "bin/simg2img";
@@ -59,12 +51,6 @@
             importance=Importance.ALWAYS)
     private String mSystemImagePath;
 
-    @Option(name="userdata_size",
-            shortName='u',
-            description="size in bytes of the new userdata partition",
-            importance=Importance.ALWAYS)
-    private long mUserdataSize = kDefaultUserdataSize;
-
     private File mUnsparseSystemImage;
 
     @After
@@ -121,47 +107,50 @@
         if (!wasRoot)
             Assert.assertTrue("Test requires root", getDevice().enableAdbRoot());
 
-        expectGsiStatus("normal");
+        assertDsuStatus("normal");
 
         // Sleep after installing to allow time for gsi_tool to reboot. This prevents a race between
         // the device rebooting and waitForDeviceAvailable() returning.
-        getDevice().executeShellV2Command("gsi_tool install --userdata-size " + mUserdataSize +
-            " --gsi-size " + gsi.length() + " && sleep 10000000", gsi, null, 10, TimeUnit.MINUTES, 1);
+        getDevice()
+                .executeShellV2Command(
+                        String.format(
+                                "gsi_tool install --userdata-size %d"
+                                        + " --gsi-size %d"
+                                        + " && sleep 10000000",
+                                getDsuUserdataSize(kDefaultUserdataSize), gsi.length()),
+                        gsi,
+                        null,
+                        10,
+                        TimeUnit.MINUTES,
+                        1);
         getDevice().waitForDeviceAvailable();
         getDevice().enableAdbRoot();
 
-        expectGsiStatus("running");
+        assertDsuStatus("running");
 
         getDevice().rebootUntilOnline();
 
-        expectGsiStatus("installed");
+        assertDsuStatus("installed");
 
-        CommandResult result = getDevice().executeShellV2Command("gsi_tool enable");
-        Assert.assertEquals("gsi_tool enable failed", 0, result.getExitCode().longValue());
+        assertShellCommand("gsi_tool enable");
 
         getDevice().reboot();
 
-        expectGsiStatus("running");
+        assertDsuStatus("running");
 
         getDevice().reboot();
 
-        expectGsiStatus("running");
+        assertDsuStatus("running");
 
-        getDevice().executeShellV2Command("gsi_tool wipe");
+        assertShellCommand("gsi_tool wipe");
 
         getDevice().rebootUntilOnline();
 
-        expectGsiStatus("normal");
+        assertDsuStatus("normal");
 
         if (wasRoot) {
             getDevice().enableAdbRoot();
         }
     }
-
-    private void expectGsiStatus(String expected) throws Exception {
-        CommandResult result = getDevice().executeShellV2Command("gsi_tool status");
-        String status = result.getStdout().split("\n", 2)[0].trim();
-        Assert.assertEquals("Device not in expected DSU state", expected, status);
-    }
 }
 
diff --git a/tests/DsuGsiIntegrationTest.java b/tests/DsuGsiIntegrationTest.java
new file mode 100644
index 0000000..78d001c
--- /dev/null
+++ b/tests/DsuGsiIntegrationTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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 com.android.tests.dsu;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class DsuGsiIntegrationTest extends DsuTestBase {
+    private static final long DSU_MAX_WAIT_SEC = 10 * 60;
+    private static final long DSU_DEFAULT_USERDATA_SIZE = 8L << 30;
+
+    private static final String GSI_IMAGE_NAME = "system.img";
+    private static final String DSU_IMAGE_ZIP_PUSH_PATH = "/sdcard/gsi.zip";
+
+    private static final String REMOUNT_TEST_PATH = "/system/remount_test";
+    private static final String REMOUNT_TEST_FILE = REMOUNT_TEST_PATH + "/test_file";
+
+    @Option(
+            name = "wipe-dsu-on-failure",
+            description = "Wipe the DSU installation on test failure.")
+    private boolean mWipeDsuOnFailure = true;
+
+    @Option(
+            name = "system-image-path",
+            description = "Path to the GSI system.img or directory containing the system.img.",
+            mandatory = true)
+    private File mSystemImagePath;
+
+    private File mSystemImageZip;
+
+    private String getDsuInstallCommand() {
+        return String.format(
+                "am start-activity"
+                        + " -n com.android.dynsystem/com.android.dynsystem.VerificationActivity"
+                        + " -a android.os.image.action.START_INSTALL"
+                        + " -d file://%s"
+                        + " --el KEY_USERDATA_SIZE %d"
+                        + " --ez KEY_ENABLE_WHEN_COMPLETED true",
+                DSU_IMAGE_ZIP_PUSH_PATH, getDsuUserdataSize(DSU_DEFAULT_USERDATA_SIZE));
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        mSystemImageZip = null;
+        InputStream stream = null;
+        try {
+            assertNotNull("--system-image-path is invalid", mSystemImagePath);
+            if (mSystemImagePath.isDirectory()) {
+                File gsiImageFile = FileUtil.findFile(mSystemImagePath, GSI_IMAGE_NAME);
+                assertNotNull("Cannot find " + GSI_IMAGE_NAME, gsiImageFile);
+                stream = new FileInputStream(gsiImageFile);
+            } else {
+                stream = new FileInputStream(mSystemImagePath);
+            }
+            stream = new BufferedInputStream(stream);
+            mSystemImageZip = FileUtil.createTempFile(this.getClass().getSimpleName(), "gsi.zip");
+            try (FileOutputStream foStream = new FileOutputStream(mSystemImageZip);
+                    BufferedOutputStream boStream = new BufferedOutputStream(foStream);
+                    ZipOutputStream out = new ZipOutputStream(boStream); ) {
+                // Don't bother compressing it as we are going to uncompress it on device anyway.
+                out.setLevel(0);
+                out.putNextEntry(new ZipEntry(GSI_IMAGE_NAME));
+                StreamUtil.copyStreams(stream, out);
+                out.closeEntry();
+            }
+        } finally {
+            StreamUtil.close(stream);
+        }
+    }
+
+    @After
+    public void tearDown() {
+        try {
+            FileUtil.deleteFile(mSystemImageZip);
+        } catch (SecurityException e) {
+            CLog.w("Failed to clean up '%s': %s", mSystemImageZip, e);
+        }
+        if (mWipeDsuOnFailure) {
+            // If test case completed successfully, then the test body should have called `wipe`
+            // already and calling `wipe` again would be a noop.
+            // If test case failed, then this piece of code would clean up the DSU installation left
+            // by the failed test case.
+            try {
+                getDevice().executeShellV2Command("gsi_tool wipe");
+                if (isDsuRunning()) {
+                    getDevice().reboot();
+                }
+            } catch (DeviceNotAvailableException e) {
+                CLog.w("Failed to clean up DSU installation on device: %s", e);
+            }
+        }
+        try {
+            getDevice().deleteFile(DSU_IMAGE_ZIP_PUSH_PATH);
+        } catch (DeviceNotAvailableException e) {
+            CLog.w("Failed to clean up device '%s': %s", DSU_IMAGE_ZIP_PUSH_PATH, e);
+        }
+    }
+
+    @Test
+    public void testDsuGsi() throws DeviceNotAvailableException {
+        if (isDsuRunning()) {
+            CLog.i("Wipe existing DSU installation");
+            assertShellCommand("gsi_tool wipe");
+            getDevice().reboot();
+            assertDsuNotRunning();
+        }
+
+        CLog.i("Pushing '%s' -> '%s'", mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH);
+        getDevice().pushFile(mSystemImageZip, DSU_IMAGE_ZIP_PUSH_PATH, true);
+
+        final long freeSpaceBeforeInstall = getDevice().getPartitionFreeSpace("/data") << 10;
+        assertShellCommand(getDsuInstallCommand());
+        CLog.i("Wait for DSU installation complete and reboot");
+        assertTrue(
+                "Timed out waiting for DSU installation complete",
+                getDevice().waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000));
+        CLog.i("DSU installation is complete and device is disconnected");
+
+        getDevice().waitForDeviceAvailable();
+        assertDsuRunning();
+        CLog.i("Successfully booted with DSU");
+
+        CLog.i("Test 'gsi_tool enable -s' and 'gsi_tool enable'");
+        getDevice().reboot();
+        assertDsuNotRunning();
+
+        final long freeSpaceAfterInstall = getDevice().getPartitionFreeSpace("/data") << 10;
+        final long estimatedDsuSize = freeSpaceBeforeInstall - freeSpaceAfterInstall;
+        assertTrue(
+                String.format(
+                        "Expected DSU installation to consume some storage space, free space before"
+                                + " install: %d, free space after install: %d, delta: %d",
+                        freeSpaceBeforeInstall, freeSpaceAfterInstall, estimatedDsuSize),
+                estimatedDsuSize > 0);
+
+        assertShellCommand("gsi_tool enable");
+        getDevice().reboot();
+        assertDsuRunning();
+
+        CLog.i("Set up 'adb remount' for testing (requires reboot)");
+        assertAdbRoot();
+        assertShellCommand("remount");
+        getDevice().reboot();
+        assertDsuRunning();
+        assertAdbRoot();
+        assertShellCommand("remount");
+        assertDevicePathNotExist(REMOUNT_TEST_PATH);
+        assertShellCommand(String.format("mkdir -p '%s'", REMOUNT_TEST_PATH));
+        assertShellCommand(String.format("cp /system/bin/init '%s'", REMOUNT_TEST_FILE));
+        final String initContent = getDevice().pullFileContents("/system/bin/init");
+
+        CLog.i("DSU and original system have separate remount overlays");
+        assertShellCommand("gsi_tool disable");
+        getDevice().reboot();
+        assertDsuNotRunning();
+        assertDevicePathNotExist(REMOUNT_TEST_PATH);
+
+        CLog.i("Test that 'adb remount' is consistent after reboot");
+        assertShellCommand("gsi_tool enable");
+        getDevice().reboot();
+        assertDsuRunning();
+        assertDevicePathExist(REMOUNT_TEST_FILE);
+        assertEquals(
+                String.format(
+                        "Expected contents of '%s' to persist after reboot", REMOUNT_TEST_FILE),
+                initContent,
+                getDevice().pullFileContents(REMOUNT_TEST_FILE));
+
+        CLog.i("'enable-verity' should teardown the remount overlay and restore the filesystem");
+        assertAdbRoot();
+        assertShellCommand("enable-verity");
+        getDevice().reboot();
+        assertDsuRunning();
+        assertDevicePathNotExist(REMOUNT_TEST_PATH);
+
+        CLog.i("Test 'gsi_tool wipe'");
+        assertShellCommand("gsi_tool wipe");
+        getDevice().reboot();
+        assertDsuNotRunning();
+
+        final double dampeningCoefficient = 0.9;
+        final long freeSpaceAfterWipe = getDevice().getPartitionFreeSpace("/data") << 10;
+        final long freeSpaceReturnedByWipe = freeSpaceAfterWipe - freeSpaceAfterInstall;
+        assertTrue(
+                String.format(
+                        "Expected 'gsi_tool wipe' to return roughly %d of storage space, free space"
+                            + " before wipe: %d, free space after wipe: %d, delta: %d",
+                        estimatedDsuSize,
+                        freeSpaceAfterInstall,
+                        freeSpaceAfterWipe,
+                        freeSpaceReturnedByWipe),
+                freeSpaceReturnedByWipe > (long) (estimatedDsuSize * dampeningCoefficient));
+    }
+}
diff --git a/tests/DsuTestBase.java b/tests/DsuTestBase.java
new file mode 100644
index 0000000..db383be
--- /dev/null
+++ b/tests/DsuTestBase.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.tests.dsu;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+abstract class DsuTestBase extends BaseHostJUnit4Test {
+    @Option(
+            name = "dsu-userdata-size-in-gb",
+            description = "Userdata partition size of the DSU installation")
+    private long mDsuUserdataSizeInGb;
+
+    protected long getDsuUserdataSize(long defaultValue) {
+        return mDsuUserdataSizeInGb > 0 ? mDsuUserdataSizeInGb << 30 : defaultValue;
+    }
+
+    public CommandResult assertShellCommand(String command) throws DeviceNotAvailableException {
+        CommandResult result = getDevice().executeShellV2Command(command);
+        assertEquals(
+                String.format("'%s' status", command), CommandStatus.SUCCESS, result.getStatus());
+        assertNotNull(String.format("'%s' exit code", command), result.getExitCode());
+        assertEquals(String.format("'%s' exit code", command), 0, result.getExitCode().intValue());
+        return result;
+    }
+
+    private String getDsuStatus() throws DeviceNotAvailableException {
+        CommandResult result;
+        try {
+            result = assertShellCommand("gsi_tool status");
+        } catch (AssertionError e) {
+            CLog.e(e);
+            return null;
+        }
+        return result.getStdout().split("\n", 2)[0].trim();
+    }
+
+    public void assertDsuStatus(String expected) throws DeviceNotAvailableException {
+        assertEquals("DSU status", expected, getDsuStatus());
+    }
+
+    public boolean isDsuRunning() throws DeviceNotAvailableException {
+        return "running".equals(getDsuStatus());
+    }
+
+    public void assertDsuRunning() throws DeviceNotAvailableException {
+        assertTrue("Expected DSU running", isDsuRunning());
+    }
+
+    public void assertDsuNotRunning() throws DeviceNotAvailableException {
+        assertFalse("Expected DSU not running", isDsuRunning());
+    }
+
+    public void assertAdbRoot() throws DeviceNotAvailableException {
+        assertTrue("Failed to 'adb root'", getDevice().enableAdbRoot());
+    }
+
+    public void assertDevicePathExist(String path) throws DeviceNotAvailableException {
+        assertTrue(String.format("Expected '%s' to exist", path), getDevice().doesFileExist(path));
+    }
+
+    public void assertDevicePathNotExist(String path) throws DeviceNotAvailableException {
+        assertFalse(
+                String.format("Expected '%s' to not exist", path), getDevice().doesFileExist(path));
+    }
+}
diff --git a/tests/dsu_gsi_integration_test.xml b/tests/dsu_gsi_integration_test.xml
new file mode 100644
index 0000000..b8c74ef
--- /dev/null
+++ b/tests/dsu_gsi_integration_test.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs DsuGsiIntegrationTest">
+    <option name="test-suite-tag" value="dsu-gsi-integration-test" />
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="com.android.tests.dsu.DsuGsiIntegrationTest" />
+    </test>
+</configuration>
+