Add CTS tests for SELinux related change ids

Test that SELINUX_R_CHANGES and SELINUX_LATEST_CHANGES place an app in
the appropriate selinux domain. The R domain is currently the latest, so
test will need to be updated once that changes.

These tests are a more end-to-end variant of com.android.server.pm.SELinuxMMACTest.

Test: atest CompatChangesSelinuxTest
Bug: 168782947
Change-Id: I6f9ddce61d95f40687d9d8fa1ac87e95e72c38e1
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
new file mode 100644
index 0000000..472d554
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2020 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.
+//
+
+java_library_static {
+    name: "selinux_app_empty",
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+}
+
+android_test_helper_app {
+    name: "CtsSelinuxQCompatApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    static_libs: ["selinux_app_empty"],
+    manifest: "AndroidManifest_Q.xml",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsSelinuxRCompatApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    static_libs: ["selinux_app_empty"],
+    manifest: "AndroidManifest_R.xml",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
new file mode 100644
index 0000000..5a6c1d3
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.appcompat.selinux_app">
+    <uses-sdk android:targetSdkVersion="29"/>
+    <application
+        android:debuggable="true">
+         <activity android:name=".Empty"
+         android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
new file mode 100644
index 0000000..0fecd4f
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.appcompat.selinux_app">
+    <uses-sdk android:targetSdkVersion="30"/>
+    <application
+        android:debuggable="true">
+         <activity android:name=".Empty"
+         android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
new file mode 100644
index 0000000..a350bc0
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 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.cts.appcompat.selinux_app;
+
+import android.app.Activity;
+
+public class Empty extends Activity {
+
+}
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
new file mode 100644
index 0000000..1006c47
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 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.cts.appcompat;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tests for the {@link android.app.compat.CompatChanges} SystemApi.
+ */
+
+public class CompatChangesSelinuxTest extends CompatChangeGatingTestCase {
+
+    protected static final String Q_TEST_APK = "CtsSelinuxQCompatApp.apk";
+    protected static final String R_TEST_APK = "CtsSelinuxRCompatApp.apk";
+
+    protected static final String TEST_PKG = "com.android.cts.appcompat.selinux_app";
+
+    private static final long SELINUX_LATEST_CHANGES = 143539591L;
+    private static final long SELINUX_R_CHANGES = 168782947L;
+
+    private static final Pattern PS_ENTRY_PATTERN = Pattern.compile("^(?<label>\\S+)\\s+(?<name>\\S+)");
+
+
+    public void testTargetSdkQAppIsInQDomainByDefault() throws Exception {
+        installPackage(Q_TEST_APK, false);
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app_29");
+        } finally {
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkQAppIsInLatestDomainWithLatestOptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+        final long configId = getClass().getCanonicalName().hashCode();
+
+        installPackage(Q_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkQAppIsInRDomainWithROptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_R_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+
+        installPackage(Q_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+            // TODO(b/168782947): Update domain if/when an R specific one is created to
+            // differentiate from untrusted_app.
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkRAppIsInRDomainByDefault() throws Exception {
+        installPackage(R_TEST_APK, false);
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+        } finally {
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkRAppIsInLatestDomainWithLatestOptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+        installPackage(R_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    private Map<String, String> getPackageToDomain() throws Exception {
+        Map<String, String> packageToDomain = new HashMap<>();
+        String output = getDevice().executeShellCommand("ps -e -o LABEL,NAME");
+        String[] lines = output.split("\n");
+        for (int i = 1; i < lines.length; ++i) {
+            String line = lines[i];
+            Matcher matcher = PS_ENTRY_PATTERN.matcher(line);
+            if (!matcher.matches())
+                continue;
+            String label = matcher.group("label");
+            String domain = label.split(":")[2];
+            String packageName = matcher.group("name");
+            packageToDomain.put(packageName, domain);
+        }
+        return packageToDomain;
+    }
+
+    private void startApp() throws Exception {
+        runCommand("am start -n " + TEST_PKG + "/" + TEST_PKG + ".Empty");
+        Thread.currentThread().sleep(100);
+    }
+
+
+}
diff --git a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
index 6fa2f2b..1217fbe 100644
--- a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
+++ b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
@@ -142,55 +142,53 @@
             Set<Long> enabledChanges, Set<Long> disabledChanges,
             Set<Long> reportedEnabledChanges, Set<Long> reportedDisabledChanges)
             throws DeviceNotAvailableException {
+
         // Set compat overrides
         setCompatConfig(enabledChanges, disabledChanges, pkgName);
-
         // Send statsd config
         final long configId = getClass().getCanonicalName().hashCode();
         createAndUploadStatsdConfig(configId, pkgName);
 
-        // Run device-side test
-        if (testClassName.startsWith(".")) {
-            testClassName = pkgName + testClassName;
-        }
-        RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
-                getDevice().getIDevice());
-        testRunner.setMethodName(testClassName, testMethodName);
-        CollectingTestListener listener = new CollectingTestListener();
-        assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
-
-        // Clear overrides.
-        resetCompatChanges(enabledChanges, pkgName);
-        resetCompatChanges(disabledChanges, pkgName);
-
-        // Clear statsd report data and remove config
-        Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
-        removeStatsdConfig(configId);
-
-        // Check that device side test occurred as expected
-        final TestRunResult result = listener.getCurrentRunResults();
-        assertWithMessage("Failed to successfully run device tests for %s: %s",
-                          result.getName(), result.getRunFailureMessage())
-                .that(result.isRunFailure()).isFalse();
-        assertWithMessage("Should run only exactly one test method!")
-                .that(result.getNumTests()).isEqualTo(1);
-        if (result.hasFailedTests()) {
-            // build a meaningful error message
-            StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
-            for (Map.Entry<TestDescription, TestResult> resultEntry :
-                    result.getTestResults().entrySet()) {
-                if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
-                    errorBuilder.append(resultEntry.getKey().toString());
-                    errorBuilder.append(":\n");
-                    errorBuilder.append(resultEntry.getValue().getStackTrace());
-                }
+        try {
+            // Run device-side test
+            if (testClassName.startsWith(".")) {
+                testClassName = pkgName + testClassName;
             }
-            throw new AssertionError(errorBuilder.toString());
+            RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
+                    getDevice().getIDevice());
+            testRunner.setMethodName(testClassName, testMethodName);
+            CollectingTestListener listener = new CollectingTestListener();
+            assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
+
+            // Check that device side test occurred as expected
+            final TestRunResult result = listener.getCurrentRunResults();
+            assertWithMessage("Failed to successfully run device tests for %s: %s",
+                            result.getName(), result.getRunFailureMessage())
+                    .that(result.isRunFailure()).isFalse();
+            assertWithMessage("Should run only exactly one test method!")
+                    .that(result.getNumTests()).isEqualTo(1);
+            if (result.hasFailedTests()) {
+                // build a meaningful error message
+                StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
+                for (Map.Entry<TestDescription, TestResult> resultEntry :
+                        result.getTestResults().entrySet()) {
+                    if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+                        errorBuilder.append(resultEntry.getKey().toString());
+                        errorBuilder.append(":\n");
+                        errorBuilder.append(resultEntry.getValue().getStackTrace());
+                    }
+                }
+                throw new AssertionError(errorBuilder.toString());
+            }
+
+        } finally {
+            // Cleanup compat overrides
+            resetCompatConfig(pkgName, enabledChanges, disabledChanges);
+            // Validate statsd report
+            validatePostRunStatsdReport(configId, pkgName, reportedEnabledChanges,
+                                        reportedDisabledChanges);
         }
 
-        // Validate statsd report
-        validatePostRunStatsdReport(reportedChanges, reportedEnabledChanges,
-            reportedDisabledChanges);
     }
 
     /**
@@ -217,7 +215,7 @@
      * @param pkgName  The package name of the app that is expected to report the atom. It will be
      *                 the only allowed log source.
      */
-    private void createAndUploadStatsdConfig(long configId, String pkgName)
+    protected void createAndUploadStatsdConfig(long configId, String pkgName)
             throws DeviceNotAvailableException {
         final String atomName = "Atom" + System.nanoTime();
         final String eventName = "Event" + System.nanoTime();
@@ -252,6 +250,8 @@
         } catch (IOException e) {
             throw new RuntimeException("IO error when writing to temp file.", e);
         }
+        // Purge data
+        getReportList(configId);
     }
 
     /**
@@ -278,7 +278,7 @@
      * @param disabledChanges Changes to be disabled.
      * @param packageName     Package name for the app whose config is being changed.
      */
-    private void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
+    protected void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
             @Nonnull String packageName) throws DeviceNotAvailableException {
         for (Long enabledChange : enabledChanges) {
             runCommand("am compat enable " + enabledChange + " " + packageName);
@@ -291,7 +291,7 @@
     /**
      * Reset changes to default for a package.
      */
-    private void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
+    protected void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
             throws DeviceNotAvailableException {
         for (Long change : changes) {
             runCommand("am compat reset " + change + " " + packageName);
@@ -332,16 +332,41 @@
     }
 
     /**
-     * Validate that all overridden changes were logged while running the test.
+     * Cleanup the altered change ids under test.
+     *
+     * @param pkgName               Package name of the app under test.
+     * @param enabledChanges        Set of changes that were enabled during the test and need to be
+     *                              reset to the default value.
+     * @param disabledChanges       Set of changes that were disabled during the test and need to
+     *                              be reset to the default value.
      */
-    private void validatePostRunStatsdReport(Map<Long, Boolean> reportedChanges,
-            Set<Long> enabledChanges, Set<Long> disabledChanges)
+    protected void resetCompatConfig( String pkgName, Set<Long> enabledChanges,
+            Set<Long> disabledChanges) throws DeviceNotAvailableException {
+        // Clear overrides.
+        resetCompatChanges(enabledChanges, pkgName);
+        resetCompatChanges(disabledChanges, pkgName);
+    }
+
+    /**
+     * Validate that all overridden changes were logged while running the test.
+     *
+     * @param configId              The unique config id used to track change id queries.
+     * @param pkgName               Package name of the app under test.
+     * @param loggedEnabledChanges  Changes expected to be logged as enabled during the test.
+     * @param loggedDisabledChanges Changes expected to be logged as disabled during the test.
+     */
+    protected void validatePostRunStatsdReport(long configId, String pkgName,
+            Set<Long> loggedEnabledChanges, Set<Long> loggedDisabledChanges)
             throws DeviceNotAvailableException {
-        for (Long enabledChange : enabledChanges) {
+        // Clear statsd report data and remove config
+        Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
+        removeStatsdConfig(configId);
+
+        for (Long enabledChange : loggedEnabledChanges) {
             assertThat(reportedChanges)
                     .containsEntry(enabledChange, true);
         }
-        for (Long disabledChange : disabledChanges) {
+        for (Long disabledChange : loggedDisabledChanges) {
             assertThat(reportedChanges)
                     .containsEntry(disabledChange, false);
         }