Capture usagestats service dump before test teardown.
Bug: 218339525
Test: atest ./tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
Change-Id: Ia45a72cd5b595f671ddcdce80d61a89f38e8a29c
diff --git a/tests/tests/app.usage/AndroidManifest.xml b/tests/tests/app.usage/AndroidManifest.xml
index 75272ee..d98b044 100644
--- a/tests/tests/app.usage/AndroidManifest.xml
+++ b/tests/tests/app.usage/AndroidManifest.xml
@@ -29,6 +29,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
diff --git a/tests/tests/app.usage/AndroidTest.xml b/tests/tests/app.usage/AndroidTest.xml
index 9854d57..101fd39 100644
--- a/tests/tests/app.usage/AndroidTest.xml
+++ b/tests/tests/app.usage/AndroidTest.xml
@@ -32,5 +32,10 @@
<option name="package" value="android.app.usage.cts" />
<option name="runtime-hint" value="1m47s" />
<option name="hidden-api-checks" value="false" />
+ <option name="isolated-storage" value="false" />
</test>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/CtsUsageStatsTestCases" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
</configuration>
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/DumpOnFailureRule.java b/tests/tests/app.usage/src/android/app/usage/cts/DumpOnFailureRule.java
new file mode 100644
index 0000000..f381e51
--- /dev/null
+++ b/tests/tests/app.usage/src/android/app/usage/cts/DumpOnFailureRule.java
@@ -0,0 +1,94 @@
+/*
+ * 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 android.app.usage.cts;
+
+import static android.app.usage.cts.UsageStatsTest.TAG;
+
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.OnFailureRule;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class DumpOnFailureRule extends OnFailureRule {
+ private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
+ "CtsUsageStatsTestCases");
+
+ @Override
+ public void onTestFailure(Statement base, Description description, Throwable throwable) {
+ if (throwable instanceof AssumptionViolatedException) {
+ final String testName = description.getClassName() + "_" + description.getMethodName();
+ Log.d(TAG, "Skipping test " + testName + ": " + throwable);
+ return;
+ }
+
+ prepareDumpRootDir();
+ final File dumpFile = new File(mDumpDir, "dump-" + getShortenedTestName(description));
+ Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
+ try (FileOutputStream out = new FileOutputStream(dumpFile)) {
+ dumpCommandOutput(out, "dumpsys usagestats");
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Error opening file: " + dumpFile, e);
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing file: " + dumpFile, e);
+ }
+ }
+
+ private String getShortenedTestName(Description description) {
+ final String qualifiedClassName = description.getClassName();
+ final String className = qualifiedClassName.substring(
+ qualifiedClassName.lastIndexOf(".") + 1);
+ final String shortenedClassName = className.chars()
+ .filter(Character::isUpperCase)
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ return shortenedClassName + "_" + description.getMethodName();
+ }
+
+ void dumpCommandOutput(FileOutputStream out, String cmd) {
+ final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().executeShellCommand(cmd);
+ try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
+ FileUtils.copy(in, out);
+ out.write("\n\n=================================================================\n\n"
+ .getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.e(TAG, "Error dumping '" + cmd + "'", e);
+ }
+ }
+
+ void prepareDumpRootDir() {
+ if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
+ Log.e(TAG, "Error creating " + mDumpDir);
+ }
+ }
+}
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
index ee78929..07e01d5 100644
--- a/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
+++ b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
@@ -87,7 +87,6 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.AppStandbyUtils;
import com.android.compatibility.common.util.BatteryUtils;
@@ -129,10 +128,10 @@
* along with the new time.
* - Proper eviction of old data.
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(UsageStatsTestRunner.class)
public class UsageStatsTest {
private static final boolean DEBUG = false;
- private static final String TAG = "UsageStatsTest";
+ static final String TAG = "UsageStatsTest";
private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} " +
AppOpsManager.OPSTR_GET_USAGE_STATS + " {1}";
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTestRunner.java b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTestRunner.java
new file mode 100644
index 0000000..090d19c
--- /dev/null
+++ b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTestRunner.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.app.usage.cts;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.rules.RunRules;
+import org.junit.rules.TestRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.List;
+
+/**
+ * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
+ */
+public class UsageStatsTestRunner extends AndroidJUnit4ClassRunner {
+ private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
+
+ public UsageStatsTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ @Override
+ public Statement methodInvoker(FrameworkMethod method, Object test) {
+ return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
+ describeChild(method));
+ }
+}