1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tests.apex;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.junit.Assume.assumeTrue;
23 
24 import android.cts.install.lib.host.InstallUtilsHost;
25 
26 import com.android.apex.ApexInfo;
27 import com.android.apex.XmlParser;
28 import com.android.tests.rollback.host.AbandonSessionsRule;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
31 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
32 
33 import org.junit.After;
34 import org.junit.Before;
35 import org.junit.Rule;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.time.Duration;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.stream.Collectors;
45 
46 /**
47  * Host side integration tests for apexd.
48  */
49 @RunWith(DeviceJUnit4ClassRunner.class)
50 public class ApexdHostTest extends BaseHostJUnit4Test  {
51 
52     private static final String SHIM_APEX_PATH = "/system/apex/com.android.apex.cts.shim.apex";
53 
54     private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this);
55 
56     @Rule
57     public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
58 
59     private boolean mWasAdbRoot = false;
60 
61     @Before
setUp()62     public void setUp() throws Exception {
63         mHostUtils.uninstallShimApexIfNecessary();
64         mWasAdbRoot = getDevice().isAdbRoot();
65         if (!mWasAdbRoot) {
66             assumeTrue("Device requires root", getDevice().enableAdbRoot());
67         }
68     }
69 
70     @After
tearDown()71     public void tearDown() throws Exception {
72         mHostUtils.uninstallShimApexIfNecessary();
73         if (!mWasAdbRoot) {
74             getDevice().disableAdbRoot();
75         }
76     }
77 
deviceHasActiveApex(String apexName)78     private boolean deviceHasActiveApex(String apexName) throws Exception {
79         return getDevice().getActiveApexes().stream().anyMatch(
80                 apex -> apex.name.equals(apexName));
81     }
82 
83     @Test
testOrphanedApexIsNotActivated()84     public void testOrphanedApexIsNotActivated() throws Exception {
85         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
86         assumeTrue("Device requires root", getDevice().isAdbRoot());
87         try {
88             assertThat(getDevice().pushFile(mHostUtils.getTestFile("apex.apexd_test_v2.apex"),
89                     "/data/apex/active/apexd_test_v2.apex")).isTrue();
90             getDevice().reboot();
91             assertWithMessage("Timed out waiting for device to boot").that(
92                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
93             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
94             ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo(
95                     "com.android.apex.test_package", 2L);
96             assertThat(activeApexes).doesNotContain(testApex);
97             mHostUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex",
98                     Duration.ofMinutes(3));
99         } finally {
100             getDevice().executeShellV2Command("rm /data/apex/active/apexd_test_v2.apex");
101         }
102     }
103     @Test
testApexWithoutPbIsNotActivated()104     public void testApexWithoutPbIsNotActivated() throws Exception {
105         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
106         assumeTrue("Device requires root", getDevice().isAdbRoot());
107         final String testApexFile = "com.android.apex.cts.shim.v2_no_pb.apex";
108         try {
109             assertThat(getDevice().pushFile(mHostUtils.getTestFile(testApexFile),
110                     "/data/apex/active/" + testApexFile)).isTrue();
111             getDevice().reboot();
112             assertWithMessage("Timed out waiting for device to boot").that(
113                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
114             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
115             ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo(
116                     "com.android.apex.cts.shim", 2L);
117             assertThat(activeApexes).doesNotContain(testApex);
118             mHostUtils.waitForFileDeleted("/data/apex/active/" + testApexFile,
119                     Duration.ofMinutes(3));
120         } finally {
121             getDevice().executeShellV2Command("rm /data/apex/active/" + testApexFile);
122         }
123     }
124 
125     @Test
testRemountApex()126     public void testRemountApex() throws Exception {
127         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
128         assumeTrue("Device requires root", getDevice().isAdbRoot());
129         final File oldFile = getDevice().pullFile(SHIM_APEX_PATH);
130         try {
131             getDevice().remountSystemWritable();
132             // In case remount requires a reboot, wait for boot to complete.
133             getDevice().waitForBootComplete(Duration.ofMinutes(3).toMillis());
134             final File newFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex");
135             // Stop framework
136             getDevice().executeShellV2Command("stop");
137             // Push new shim APEX. This simulates adb sync.
138             getDevice().pushFile(newFile, SHIM_APEX_PATH);
139             // Ask apexd to remount packages
140             getDevice().executeShellV2Command("cmd -w apexservice remountPackages");
141             // Start framework
142             getDevice().executeShellV2Command("start");
143             // Give enough time for system_server to boot.
144             Thread.sleep(Duration.ofSeconds(15).toMillis());
145             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
146             ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo(
147                     "com.android.apex.cts.shim", 2L);
148             assertThat(activeApexes).contains(testApex);
149         } finally {
150             getDevice().pushFile(oldFile, SHIM_APEX_PATH);
151             getDevice().reboot();
152         }
153     }
154 
155     @Test
testApexWithoutPbIsNotActivated_ProductPartitionHasOlderVersion()156     public void testApexWithoutPbIsNotActivated_ProductPartitionHasOlderVersion()
157             throws Exception {
158         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
159         assumeTrue("Device requires root", getDevice().isAdbRoot());
160 
161         try {
162             getDevice().remountSystemWritable();
163             // In case remount requires a reboot, wait for boot to complete.
164             assertWithMessage("Timed out waiting for device to boot").that(
165                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
166 
167             final File v1 = mHostUtils.getTestFile("apex.apexd_test.apex");
168             getDevice().pushFile(v1, "/product/apex/apex.apexd_test.apex");
169 
170             final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex");
171             getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex");
172 
173             getDevice().reboot();
174             assertWithMessage("Timed out waiting for device to boot").that(
175                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
176 
177             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
178             assertThat(activeApexes).contains(new ITestDevice.ApexInfo(
179                     "com.android.apex.test_package", 1L));
180             assertThat(activeApexes).doesNotContain(new ITestDevice.ApexInfo(
181                     "com.android.apex.test_package", 2L));
182 
183             // v2_no_pb should be deleted
184             mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex",
185                     Duration.ofMinutes(3));
186         } finally {
187             getDevice().remountSystemWritable();
188             assertWithMessage("Timed out waiting for device to boot").that(
189                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
190 
191             getDevice().executeShellV2Command("rm /product/apex/apex.apexd_test.apex");
192             getDevice().executeShellV2Command("rm /data/apex/active/apex.apexd_test_v2_no_pb.apex");
193         }
194     }
195 
196     @Test
testApexWithoutPbIsNotActivated_ProductPartitionHasNewerVersion()197     public void testApexWithoutPbIsNotActivated_ProductPartitionHasNewerVersion()
198             throws Exception {
199         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
200         assumeTrue("Device requires root", getDevice().isAdbRoot());
201 
202         try {
203             getDevice().remountSystemWritable();
204             // In case remount requires a reboot, wait for boot to complete.
205             assertWithMessage("Timed out waiting for device to boot").that(
206                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
207 
208             final File v3 = mHostUtils.getTestFile("apex.apexd_test_v3.apex");
209             getDevice().pushFile(v3, "/product/apex/apex.apexd_test_v3.apex");
210 
211             final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex");
212             getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex");
213 
214             getDevice().reboot();
215             assertWithMessage("Timed out waiting for device to boot").that(
216                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
217 
218             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
219             assertThat(activeApexes).contains(new ITestDevice.ApexInfo(
220                     "com.android.apex.test_package", 3L));
221             assertThat(activeApexes).doesNotContain(new ITestDevice.ApexInfo(
222                     "com.android.apex.test_package", 2L));
223 
224             // v2_no_pb should be deleted
225             mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex",
226                     Duration.ofMinutes(3));
227         } finally {
228             getDevice().remountSystemWritable();
229             assertWithMessage("Timed out waiting for device to boot").that(
230                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
231 
232             getDevice().executeShellV2Command("rm /product/apex/apex.apexd_test_v3.apex");
233             getDevice().executeShellV2Command("rm /data/apex/active/apex.apexd_test_v2_no_pb.apex");
234         }
235     }
236 
237     @Test
testApexInfoListIsValid()238     public void testApexInfoListIsValid() throws Exception {
239         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
240         assumeTrue("Device requires root", getDevice().isAdbRoot());
241 
242         try (FileInputStream fis = new FileInputStream(
243                 getDevice().pullFile("/apex/apex-info-list.xml"))) {
244             // #1 Data got from apexd via binder
245             Set<ITestDevice.ApexInfo> fromApexd = getDevice().getActiveApexes();
246             // #2 Data got from the xml file (key is the path)
247             Map<String, ApexInfo> fromXml = XmlParser.readApexInfoList(fis).getApexInfo().stream()
248                     .collect(Collectors.toMap(ApexInfo::getModulePath, ai -> ai));
249 
250             // Make sure that all items in #1 are also in #2 and they are identical
251             for (ITestDevice.ApexInfo ai : fromApexd) {
252                 ApexInfo apexFromXml = fromXml.get(ai.sourceDir);
253                 assertWithMessage("APEX (" + ai.toString() + ") is not found in the list")
254                         .that(apexFromXml).isNotNull();
255                 assertWithMessage("Version mismatch for APEX (" + ai.toString() + ")")
256                         .that(ai.versionCode).isEqualTo(apexFromXml.getVersionCode());
257                 assertWithMessage("APEX (" + ai.toString() + ") is not active")
258                         .that(apexFromXml.getIsActive()).isTrue();
259             }
260         }
261     }
262 
263     /**
264      * Test to verify that the state of a staged session does not change if apexd is stopped and
265      * restarted while a session is staged.
266      */
267     @Test
testApexSessionStateUnchangedBeforeReboot()268     public void testApexSessionStateUnchangedBeforeReboot() throws Exception {
269         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
270         assumeTrue("Device requires root", getDevice().isAdbRoot());
271 
272         File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex");
273         String error = mHostUtils.installStagedPackage(apexFile);
274         assertThat(error).isNull();
275         String sessionId = getDevice().executeShellCommand(
276                 "pm get-stagedsessions --only-ready --only-parent --only-sessionid").trim();
277         assertThat(sessionId).isNotEmpty();
278         String sessionStateCmd = "cmd -w apexservice getStagedSessionInfo " + sessionId;
279         String initialState = getDevice().executeShellV2Command(sessionStateCmd).getStdout();
280         assertThat(initialState).isNotEmpty();
281 
282         // Kill apexd. This means apexd will perform its start logic when the second install
283         // is staged.
284         getDevice().executeShellV2Command("kill `pidof apexd`");
285 
286         // Verify that the session state remains consistent after apexd has restarted.
287         String updatedState = getDevice().executeShellV2Command(sessionStateCmd).getStdout();
288         assertThat(updatedState).isEqualTo(initialState);
289     }
290 
291     /**
292      * Verifies that content of {@code /data/apex/sessions/} is migrated to the {@code
293      * /metadata/apex/sessions}.
294      */
295     @Test
testSessionsDirMigrationToMetadata()296     public void testSessionsDirMigrationToMetadata() throws Exception {
297         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
298         assumeTrue("Device requires root", getDevice().isAdbRoot());
299 
300         try {
301             getDevice().executeShellV2Command("mkdir -p /data/apex/sessions/1543");
302             File file = File.createTempFile("foo", "bar");
303             getDevice().pushFile(file, "/data/apex/sessions/1543/file");
304 
305             // During boot sequence apexd will move /data/apex/sessions/1543/file to
306             // /metadata/apex/sessions/1543/file.
307             getDevice().reboot();
308             assertWithMessage("Timed out waiting for device to boot").that(
309                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
310 
311             assertThat(getDevice().doesFileExist("/metadata/apex/sessions/1543/file")).isTrue();
312             assertThat(getDevice().doesFileExist("/data/apex/sessions/1543/file")).isFalse();
313         } finally {
314             getDevice().executeShellV2Command("rm -R /data/apex/sessions/1543");
315             getDevice().executeShellV2Command("rm -R /metadata/apex/sessions/1543");
316         }
317     }
318 
319     @Test
testFailsToActivateApexOnDataFallbacksToPreInstalled()320     public void testFailsToActivateApexOnDataFallbacksToPreInstalled() throws Exception {
321         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
322         assumeTrue("Device requires root", getDevice().isAdbRoot());
323 
324         try {
325             final File file =
326                     mHostUtils.getTestFile("com.android.apex.cts.shim.v2_additional_file.apex");
327             getDevice().pushFile(file, "/data/apex/active/com.android.apex.cts.shim@2.apex");
328 
329             getDevice().reboot();
330             assertWithMessage("Timed out waiting for device to boot").that(
331                     getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue();
332 
333             // After reboot pre-installed version of shim apex should be activated, and corrupted
334             // version on /data should be deleted.
335             final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes();
336             ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo(
337                     "com.android.apex.cts.shim", 1L);
338             assertThat(activeApexes).contains(testApex);
339             assertThat(
340                     getDevice()
341                             .doesFileExist("/data/apex/active/com.android.apex.cts.shim@2.apex"))
342                     .isFalse();
343         } finally {
344             getDevice().deleteFile("/data/apex/active/com.android.apex.cts.shim@2.apex");
345         }
346     }
347 
348     /**
349      * Test to verify that apexd will reject a vendor apex update with an
350      *     updatable-via-apex value that doesn't match the apex's interface.
351      *     Install method is reboot-needing (staged) installation.
352      */
353     @Test
testRejectsStagedApexWithIncorrectUpdatableViaApexValue()354     public void testRejectsStagedApexWithIncorrectUpdatableViaApexValue() throws Exception {
355         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
356         assumeTrue("Device requires root", getDevice().isAdbRoot());
357         assumeTrue("Device test requires wifi hardware",
358                 getDevice().hasFeature("android.hardware.wifi"));
359         assumeTrue("Device test requires an active wifi apex",
360                 deviceHasActiveApex("com.android.hardware.wifi"));
361 
362         String apex_filename = "test.bad1.com.android.hardware.wifi.apex";
363 
364         File apexFile = mHostUtils.getTestFile(apex_filename);
365 
366         // Try to install it, we should get an error
367         String error = mHostUtils.installStagedPackage(apexFile);
368         assertThat(error).isNotNull();
369         // Verify error text involves the device manifest (Note the actual
370         //   parser error is visible in the log, but it doesn't get passed
371         //   up through libvintf, so we end up with a manifest-related error)
372         assertThat(error).contains("No device manifest");
373     }
374 
375     /**
376      * Test to verify that apexd will reject a vendor apex update with a
377      *     vintf fragment containing syntax-invalid XML. Staged installation.
378      */
379     @Test
testRejectsStagedApexWithInvalidSyntaxVintfFragment()380     public void testRejectsStagedApexWithInvalidSyntaxVintfFragment() throws Exception {
381         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
382         assumeTrue("Device requires root", getDevice().isAdbRoot());
383         assumeTrue("Device test requires wifi hardware",
384                 getDevice().hasFeature("android.hardware.wifi"));
385         assumeTrue("Device test requires an active wifi apex",
386                 deviceHasActiveApex("com.android.hardware.wifi"));
387 
388         String apex_filename = "test.bad2.com.android.hardware.wifi.apex";
389 
390         File apexFile = mHostUtils.getTestFile(apex_filename);
391 
392         // Try to install it, we should get our device manifest error
393         String error = mHostUtils.installStagedPackage(apexFile);
394         assertThat(error).isNotNull();
395         assertThat(error).contains("No device manifest");
396     }
397 
398     /**
399      * Test to verify that apexd will reject a vendor apex that tries to
400      *     update an unrelated hardware interface.  Staged installation.
401      */
402     @Test
testRejectsStagedApexThatUpdatesUnrelatedHardware()403     public void testRejectsStagedApexThatUpdatesUnrelatedHardware() throws Exception {
404         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
405         assumeTrue("Device requires root", getDevice().isAdbRoot());
406         assumeTrue("Device test requires wifi hardware",
407                 getDevice().hasFeature("android.hardware.wifi"));
408         assumeTrue("Device test requires an active wifi apex",
409                 deviceHasActiveApex("com.android.hardware.wifi"));
410 
411         String apex_filename = "test.bad3.com.android.hardware.wifi.apex";
412 
413         File apexFile = mHostUtils.getTestFile(apex_filename);
414 
415         // Try to install it, we should get a manifest/matix compatibility error
416         String error = mHostUtils.installStagedPackage(apexFile);
417         assertThat(error).isNotNull();
418         assertThat(error).contains(
419                 "Device manifest and framework compatibility matrix are incompatible");
420     }
421 
422     /**
423      * Test to verify that apexd will accept a good vendor apex update
424      *     Install method is immediate (rebootless) (non-staged) installation.
425      */
426     @Test
testAcceptsGoodRebootlessApex()427     public void testAcceptsGoodRebootlessApex() throws Exception {
428         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
429         assumeTrue("Device requires root", getDevice().isAdbRoot());
430         assumeTrue("Device test requires wifi hardware",
431                 getDevice().hasFeature("android.hardware.wifi"));
432         assumeTrue("Device test requires an active wifi apex",
433                 deviceHasActiveApex("com.android.hardware.wifi"));
434 
435         String apex_filename = "test.good1.com.android.hardware.wifi.apex";
436 
437         File apexFile = mHostUtils.getTestFile(apex_filename);
438 
439         // Try to install it, we should get an error
440         String error = mHostUtils.installRebootlessPackage(apexFile);
441         assertThat(error).isNull();
442     }
443 
444     /**
445      * Test to verify that apexd will reject a vendor apex update with an
446      *     updatable-via-apex value that doesn't match the apex's interface.
447      *     Install method is immediate (rebootless) (non-staged) installation.
448      */
449     @Test
testRejectsRebootlessApexWithIncorrectUpdatableViaApexValue()450     public void testRejectsRebootlessApexWithIncorrectUpdatableViaApexValue() throws Exception {
451         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
452         assumeTrue("Device requires root", getDevice().isAdbRoot());
453         assumeTrue("Device test requires wifi hardware",
454                 getDevice().hasFeature("android.hardware.wifi"));
455         assumeTrue("Device test requires an active wifi apex",
456                 deviceHasActiveApex("com.android.hardware.wifi"));
457 
458         String apex_filename = "test.bad1.com.android.hardware.wifi.apex";
459 
460         File apexFile = mHostUtils.getTestFile(apex_filename);
461 
462         // Try to install it, we should get an error
463         String error = mHostUtils.installRebootlessPackage(apexFile);
464         assertThat(error).isNotNull();
465         // Verify error text involves the device manifest (Note the actual
466         //   parser error is visible in the log, but it doesn't get passed
467         //   up through libvintf, so we end up with a manifest-related error)
468         assertThat(error).contains("No device manifest");
469     }
470 
471     /**
472      * Test to verify that apexd will reject a vendor apex update with a
473      *     vintf fragment containing syntax-invalid XML.
474      */
475     @Test
testRejectsRebootlessApexWithInvalidSyntaxVintfFragment()476     public void testRejectsRebootlessApexWithInvalidSyntaxVintfFragment() throws Exception {
477         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
478         assumeTrue("Device requires root", getDevice().isAdbRoot());
479         assumeTrue("Device test requires wifi hardware",
480                 getDevice().hasFeature("android.hardware.wifi"));
481         assumeTrue("Device test requires an active wifi apex",
482                 deviceHasActiveApex("com.android.hardware.wifi"));
483 
484         String apex_filename = "test.bad2.com.android.hardware.wifi.apex";
485 
486         File apexFile = mHostUtils.getTestFile(apex_filename);
487 
488         // Try to install it, we should get our device manifest error
489         String error = mHostUtils.installRebootlessPackage(apexFile);
490         assertThat(error).isNotNull();
491         assertThat(error).contains("No device manifest");
492     }
493 
494     /**
495      * Test to verify that apexd will reject a vendor apex tries to
496      *     update an unrelated hardware interface.
497      */
498     @Test
testRejectsRebootlessApexThatUpdatesUnrelatedHardware()499     public void testRejectsRebootlessApexThatUpdatesUnrelatedHardware() throws Exception {
500         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
501         assumeTrue("Device requires root", getDevice().isAdbRoot());
502         assumeTrue("Device test requires wifi hardware",
503                 getDevice().hasFeature("android.hardware.wifi"));
504         assumeTrue("Device test requires an active wifi apex",
505                 deviceHasActiveApex("com.android.hardware.wifi"));
506 
507         String apex_filename = "test.bad3.com.android.hardware.wifi.apex";
508 
509         File apexFile = mHostUtils.getTestFile(apex_filename);
510 
511         // Try to install it, we should get a manifest/matix compatibility error
512         String error = mHostUtils.installRebootlessPackage(apexFile);
513         assertThat(error).isNotNull();
514         assertThat(error).contains(
515                 "Device manifest and framework compatibility matrix are incompatible");
516     }
517 }
518