Merge "Migration of hermetic startup test to CrystalBall."
diff --git a/build/tasks/platform_tests.mk b/build/tasks/platform_tests.mk
index 9c88a6c..1bed4de 100644
--- a/build/tasks/platform_tests.mk
+++ b/build/tasks/platform_tests.mk
@@ -16,7 +16,8 @@
# based on the configuration.
#
-include $(call my-dir)/tests/platform_test_list.mk
+LOCAL_PATH := $(call my-dir)
+include $(LOCAL_PATH)/tests/platform_test_list.mk
-include $(wildcard vendor/*/build/tasks/tests/platform_test_list.mk)
my_modules := $(platform_tests)
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index f6761d9..5ef9952 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -70,9 +70,6 @@
FrameworksPrivacyLibraryTests \
SettingsUITests \
ExtServicesUnitTests\
- NexusLauncherOutOfProcTests\
- NexusLauncherDebug\
- NexusLauncherTests\
FrameworksNetSmokeTests\
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index 0ff1395..093ccb4 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -14,9 +14,7 @@
native_tests := \
adbd_test \
- apex_file_test \
- apex_manifest_test \
- apexservice_test \
+ audio_health_tests \
backtrace_test \
bionic-unit-tests \
bionic-unit-tests-static \
@@ -41,6 +39,7 @@
dvr_api-test \
dvr_buffer_queue-test \
dvr_display-test \
+ gwp_asan_unittest \
hello_world_test \
hwui_unit_tests \
incident_helper_test \
@@ -51,7 +50,6 @@
installd_otapreopt_test \
installd_service_test \
installd_utils_test \
- JniInvocation_test \
libandroidfw_tests \
libappfuse_test \
libbase_test \
@@ -64,6 +62,7 @@
libjavacore-unit-tests \
liblog-unit-tests \
libminijail_unittest_gtest \
+ libnativehelper_tests \
libnetdbpf_test \
libperfmgr_test \
libprocinfo_test \
@@ -130,7 +129,6 @@
NeuralNetworksTest_mt_static \
NeuralNetworksTest_operations \
NeuralNetworksTest_static \
- NeuralNetworksTest_static_asan \
SurfaceFlinger_test \
lmkd_unit_test \
vrflinger_test
diff --git a/build/tasks/tests/platform_test_list.mk b/build/tasks/tests/platform_test_list.mk
index d77a7d1..58aa694 100644
--- a/build/tasks/tests/platform_test_list.mk
+++ b/build/tasks/tests/platform_test_list.mk
@@ -1,7 +1,4 @@
platform_tests += \
- apex_file_test \
- apex_manifest_test \
- apexservice_test \
ActivityManagerPerfTests \
ActivityManagerPerfTestsTestApp \
AndroidTVJankTests \
@@ -20,8 +17,6 @@
BandwidthEnforcementTest \
BandwidthTests \
BluetoothTests \
- bluetooth_cert_stack \
- bluetooth_stack_with_facade \
BootHelperApp \
BusinessCard \
CalculatorFunctionalTests \
@@ -58,9 +53,7 @@
FrameworksUtilTests \
InternalLocTestApp \
JankMicroBenchmarkTests \
- libbluetooth_gd \
long_trace_config.textproto \
- libgrpc++_unsecure \
MemoryUsage \
MultiDexLegacyTestApp \
MultiDexLegacyTestApp2 \
@@ -86,9 +79,8 @@
PermissionFunctionalTests \
PermissionTestAppMV1 \
PermissionUtils \
- PlatformScenarioTests \
+ PlatformCommonScenarioTests \
PowerPerfTest \
- root-canal \
SettingsUITests \
SimpleTestApp \
skia_dm \
@@ -98,7 +90,9 @@
SmokeTestApp \
SysAppJankTestsWear \
TouchLatencyJankTestWear \
+ trace_config.textproto \
trace_config_detailed.textproto \
+ trace_config_experimental.textproto \
UbSystemUiJankTests \
UbWebViewJankTests \
UiBench \
@@ -125,3 +119,7 @@
CuttlefishRilTests \
CuttlefishWifiTests
endif
+
+ifeq ($(HOST_OS),linux)
+platform_tests += root-canal
+endif
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java b/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
index e56c4ac..b43690f 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
@@ -22,10 +22,12 @@
import com.google.common.primitives.Bytes;
import com.google.common.util.concurrent.Uninterruptibles;
-import java.awt.*;
+import java.awt.Point;
import java.time.Duration;
+import java.time.Instant;
import java.util.Arrays;
import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -80,8 +82,6 @@
private static final Duration ACTION_DELAY = Duration.ofSeconds(3L);
private static final Duration STEP_DELAY = Duration.ofMillis(10L);
static final Duration LONG_CLICK = Duration.ofSeconds(1L);
- static final int SCROLL_STEPS = 40;
- static final int FLING_STEPS = 10;
private final UsbHelper mHelper;
private UsbDevice mDelegate;
@@ -183,6 +183,12 @@
&& ADB_PID.contains(mDelegate.getProductId());
}
+ /** Get current time. */
+ @VisibleForTesting
+ Instant now() {
+ return Instant.now();
+ }
+
/** Wait for a specified duration. */
public void sleep(@Nonnull Duration duration) {
Uninterruptibles.sleepUninterruptibly(duration.toNanos(), TimeUnit.NANOSECONDS);
@@ -204,30 +210,26 @@
touch(TOUCH_UP, point, ACTION_DELAY);
}
- /** Scroll from one location to another. */
- public void scroll(@Nonnull Point from, @Nonnull Point to) {
- swipe(from, to, SCROLL_STEPS);
- }
-
- /** Fling from one location to another. */
- public void fling(@Nonnull Point from, @Nonnull Point to) {
- swipe(from, to, FLING_STEPS);
- }
-
- /** Drag from one location to another. */
- public void drag(@Nonnull Point from, @Nonnull Point to) {
- touch(TOUCH_DOWN, from, LONG_CLICK);
- scroll(from, to);
- }
-
- // Move from one location to another using discrete steps
- private void swipe(Point from, Point to, int steps) {
- steps = Math.max(steps, 1);
- float xStep = ((float) (to.x - from.x)) / steps;
- float yStep = ((float) (to.y - from.y)) / steps;
-
- for (int i = 0; i <= steps; i++) {
- Point point = new Point((int) (from.x + xStep * i), (int) (from.y + yStep * i));
+ /**
+ * Swipe from one position to another in the specified duration.
+ *
+ * @param from starting position
+ * @param to final position
+ * @param duration swipe motion duration
+ */
+ public void swipe(@Nonnull Point from, @Nonnull Point to, @Nonnull Duration duration) {
+ Instant start = now();
+ touch(TOUCH_DOWN, from, STEP_DELAY);
+ while (true) {
+ Duration elapsed = Duration.between(start, now());
+ if (duration.compareTo(elapsed) < 0) {
+ break;
+ }
+ double progress = (double) elapsed.toMillis() / duration.toMillis();
+ Point point =
+ new Point(
+ (int) (progress * to.x + (1 - progress) * from.x),
+ (int) (progress * to.y + (1 - progress) * from.y));
touch(TOUCH_DOWN, point, STEP_DELAY);
}
touch(TOUCH_UP, to, ACTION_DELAY);
@@ -242,38 +244,23 @@
}
/**
- * Write a string by pressing keys. Only alphanumeric characters and whitespace is supported.
+ * Press a combination of keys.
*
- * @param value string to write
+ * @param keys key HID usages, see <a
+ * https://source.android.com/devices/input/keyboard-devices">Keyboard devices</a>
*/
- public void write(@Nonnull String value) {
- // map characters to HID usages
- Integer[] keyCodes =
- value.codePoints()
- .mapToObj(
- c -> {
- if (Character.isSpaceChar(c)) {
- return 0x2C;
- } else if (Character.isAlphabetic(c)) {
- return Character.toLowerCase(c) - 'a' + 0x04;
- } else if (Character.isDigit(c)) {
- return c == '0' ? 0x27 : c - '1' + 0x1E;
- }
- return null;
- })
- .toArray(Integer[]::new);
- // press the keys
- key(keyCodes);
+ public void pressKeys(Integer... keys) {
+ pressKeys(Arrays.asList(keys));
}
/**
- * Press a key.
+ * Press a combination of keys.
*
- * @param keyCodes key HID usages, see <a
+ * @param keys list of key HID usages, see <a
* https://source.android.com/devices/input/keyboard-devices">Keyboard devices</a>
*/
- public void key(Integer... keyCodes) {
- Iterator<Integer> it = Arrays.stream(keyCodes).filter(Objects::nonNull).iterator();
+ public void pressKeys(@Nonnull List<Integer> keys) {
+ Iterator<Integer> it = keys.stream().filter(Objects::nonNull).iterator();
while (it.hasNext()) {
Integer keyCode = it.next();
send(HID.KEYBOARD, new byte[] {keyCode.byteValue()}, STEP_DELAY);
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java b/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
index b7a2755..62d7a30 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
@@ -30,12 +30,14 @@
/** Connected USB device. */
public class UsbDevice implements AutoCloseable {
+ private final UsbHelper mHelper;
private final IUsbNative mUsb;
private final byte[] mDescriptor = new byte[18];
private Pointer mHandle;
- UsbDevice(@Nonnull IUsbNative usb, @Nonnull Pointer devicePointer) {
- mUsb = usb;
+ UsbDevice(@Nonnull UsbHelper helper, @Nonnull Pointer devicePointer) {
+ mHelper = helper;
+ mUsb = helper.getUsb();
// retrieve device descriptor
mUsb.libusb_get_device_descriptor(devicePointer, mDescriptor);
@@ -47,11 +49,22 @@
}
/**
- * Performs a synchronous control transaction with unlimited timeout.
+ * Performs a synchronous control transaction with the default timeout.
*
* @return number of bytes transferred, or an error code
*/
public int controlTransfer(byte requestType, byte request, int value, int index, byte[] data) {
+ int timeout = (int) mHelper.getTransferTimeout().toMillis();
+ return controlTransfer(requestType, request, value, index, data, timeout);
+ }
+
+ /**
+ * Performs a synchronous control transaction.
+ *
+ * @return number of bytes transferred, or an error code
+ */
+ public int controlTransfer(
+ byte requestType, byte request, int value, int index, byte[] data, int timeout) {
return mUsb.libusb_control_transfer(
checkNotNull(mHandle),
requestType,
@@ -60,7 +73,7 @@
(short) index,
data,
(short) data.length,
- 0);
+ timeout);
}
/**
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java b/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
index 1a3b86d..18e7c67 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
@@ -42,11 +42,16 @@
*/
public class UsbHelper implements AutoCloseable {
+ // Interval used when waiting for a device
private static final Duration POLL_INTERVAL = Duration.ofSeconds(1L);
+ // Default transfer timeout used by all managed devices
+ private static final Duration DEFAULT_TRANSFER_TIMEOUT = Duration.ofSeconds(10L);
private final IUsbNative mUsb;
private Pointer mContext;
+ private Duration mTransferTimeout = DEFAULT_TRANSFER_TIMEOUT;
+
public UsbHelper() {
this((IUsbNative) Native.loadLibrary("usb-1.0", IUsbNative.class));
}
@@ -60,6 +65,21 @@
mContext = context.getValue();
}
+ /** @return native libusb adapter */
+ IUsbNative getUsb() {
+ return mUsb;
+ }
+
+ /** @return default transfer timeout (Duration.ZERO indicates unlimited timeout). */
+ public Duration getTransferTimeout() {
+ return mTransferTimeout;
+ }
+
+ /** Sets the default transfer timeout used by all managed devices. */
+ public void setTransferTimeout(@Nonnull Duration transferTimeout) {
+ mTransferTimeout = transferTimeout;
+ }
+
/**
* Verifies a USB response, throwing an exception if it corresponds to an error.
*
@@ -122,7 +142,7 @@
@VisibleForTesting
UsbDevice connect(@Nonnull Pointer devicePointer) {
- return new UsbDevice(mUsb, devicePointer);
+ return new UsbDevice(this, devicePointer);
}
/**
diff --git a/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java b/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
index 8368cf7..f40c153 100644
--- a/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
+++ b/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
@@ -22,10 +22,8 @@
import static com.android.helper.aoa.AoaDevice.ACCESSORY_START_MAX_RETRIES;
import static com.android.helper.aoa.AoaDevice.ACCESSORY_UNREGISTER_HID;
import static com.android.helper.aoa.AoaDevice.DEVICE_NOT_FOUND;
-import static com.android.helper.aoa.AoaDevice.FLING_STEPS;
import static com.android.helper.aoa.AoaDevice.GOOGLE_VID;
import static com.android.helper.aoa.AoaDevice.LONG_CLICK;
-import static com.android.helper.aoa.AoaDevice.SCROLL_STEPS;
import static com.android.helper.aoa.AoaDevice.SYSTEM_BACK;
import static com.android.helper.aoa.AoaDevice.SYSTEM_HOME;
import static com.android.helper.aoa.AoaDevice.SYSTEM_WAKE;
@@ -63,11 +61,11 @@
import java.awt.*;
import java.time.Duration;
+import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import javax.annotation.Nonnull;
@@ -230,67 +228,26 @@
}
@Test
- public void testScroll() {
+ public void testSwipe() {
mDevice = createDevice();
- mDevice.scroll(new Point(0, 0), new Point(0, SCROLL_STEPS));
+ mDevice.swipe(new Point(20, 0), new Point(70, 100), Duration.ofMillis(50));
- // generate an event for each step, spaced one pixel apart
List<Touch> events =
- Stream.iterate(0, i -> i + 1)
- .limit(SCROLL_STEPS + 1)
- .map(i -> new Touch(TOUCH_DOWN, 0, i))
- .collect(Collectors.toList());
- events.add(new Touch(TOUCH_UP, 0, SCROLL_STEPS));
-
+ List.of(
+ new Touch(TOUCH_DOWN, 20, 0),
+ new Touch(TOUCH_DOWN, 30, 20),
+ new Touch(TOUCH_DOWN, 40, 40),
+ new Touch(TOUCH_DOWN, 50, 60),
+ new Touch(TOUCH_DOWN, 60, 80),
+ new Touch(TOUCH_DOWN, 70, 100),
+ new Touch(TOUCH_UP, 70, 100));
verifyTouches(events);
}
@Test
- public void testFling() {
+ public void testPressKeys() {
mDevice = createDevice();
- mDevice.fling(new Point(0, 0), new Point(FLING_STEPS, 0));
-
- // generate an event for each step, spaced one pixel apart
- List<Touch> events =
- Stream.iterate(0, i -> i + 1)
- .limit(FLING_STEPS + 1)
- .map(i -> new Touch(TOUCH_DOWN, i, 0))
- .collect(Collectors.toList());
- events.add(new Touch(TOUCH_UP, FLING_STEPS, 0));
-
- verifyTouches(events);
- }
-
- @Test
- public void testDrag() {
- mDevice = createDevice();
- mDevice.drag(new Point(0, 0), new Point(SCROLL_STEPS, 0));
-
- // generate an event for each step, spaced one pixel apart
- List<Touch> events =
- Stream.iterate(0, i -> i + 1)
- .limit(SCROLL_STEPS + 1)
- .map(i -> new Touch(TOUCH_DOWN, i, 0))
- .collect(Collectors.toList());
- // drag is a long click followed by a scroll
- events.add(0, new Touch(TOUCH_DOWN, 0, 0));
- events.add(new Touch(TOUCH_UP, SCROLL_STEPS, 0));
-
- verifyTouches(events);
- }
-
- @Test
- public void testWrite() {
- mDevice = spy(createDevice());
- mDevice.write("Test #0123!");
-
- verify(mDevice).key(0x17, 0x08, 0x16, 0x17, 0x2C, null, 0x27, 0x1E, 0x1F, 0x20, null);
- }
-
- @Test
- public void testKey() {
- mDevice = createDevice();
- mDevice.key(1, null, 2);
+ mDevice.pressKeys(1, null, 2);
InOrder order = inOrder(mDelegate);
// press and release 1
@@ -347,11 +304,25 @@
// Helpers
+ /** Creates a mock device with predictable timestamps. */
private AoaDevice createDevice() {
- AoaDevice device = new AoaDevice(mHelper, mDelegate) {
- @Override
- public void sleep(@Nonnull Duration duration) {}
- };
+ AoaDevice device =
+ new AoaDevice(mHelper, mDelegate) {
+ private Instant mInstant;
+
+ @Override
+ Instant now() {
+ if (mInstant == null) {
+ mInstant = Instant.MIN;
+ }
+ return mInstant;
+ }
+
+ @Override
+ public void sleep(@Nonnull Duration duration) {
+ mInstant = now().plus(duration);
+ }
+ };
return spy(device);
}
diff --git a/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java b/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
index dfb3067..7a85915 100644
--- a/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
+++ b/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
@@ -35,6 +35,8 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.time.Duration;
+
/** Unit tests for {@link UsbDevice} */
@RunWith(JUnit4.class)
public class UsbDeviceTest {
@@ -43,6 +45,7 @@
private Pointer mHandle;
private IUsbNative mUsb;
+ private UsbHelper mHelper;
@Before
public void setUp() {
@@ -50,6 +53,7 @@
mHandle = new Memory(1);
mUsb = mock(IUsbNative.class);
+ mHelper = new UsbHelper(mUsb);
// return device handle when opening connection
when(mUsb.libusb_open(any(), any()))
.then(
@@ -60,7 +64,7 @@
return 0;
});
- mDevice = new UsbDevice(mUsb, mock(Pointer.class));
+ mDevice = new UsbDevice(mHelper, mock(Pointer.class));
}
@Test
@@ -75,6 +79,8 @@
@Test
public void testControlTransfer() {
+ mHelper.setTransferTimeout(Duration.ofMillis(123L));
+
byte[] data = new byte[]{1, 2, 3, 4};
mDevice.controlTransfer((byte) 1, (byte) 2, 3, 4, data);
@@ -82,7 +88,7 @@
verify(mUsb).libusb_control_transfer(eq(mHandle),
eq((byte) 1), eq((byte) 2), eq((short) 3), eq((short) 4),
eq(data), eq((short) 4), // data and length
- eq(0)); // timeout
+ eq(123)); // default timeout
}
@Test(expected = NullPointerException.class)
diff --git a/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java b/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
index fe94bd3..3310915 100644
--- a/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
+++ b/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
@@ -53,6 +53,7 @@
private static final String SCREENSHOT_DIR = "apphelper-screenshots";
private static final String FAVOR_CMD = "favor-shell-commands";
private static final String USE_HOME_CMD = "press-home-to-exit";
+ private static final String APP_IDLE_OPTION = "app-idle_ms";
private static final String LAUNCH_TIMEOUT_OPTION = "app-launch-timeout_ms";
private static final String ERROR_NOT_FOUND =
"Element %s %s is not found in the application %s";
@@ -68,6 +69,7 @@
KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private final boolean mFavorShellCommands;
private final boolean mPressHomeToExit;
+ private final long mAppIdle;
private final long mLaunchTimeout;
public AbstractStandardAppHelper(Instrumentation instr) {
@@ -79,6 +81,12 @@
mPressHomeToExit =
Boolean.valueOf(
InstrumentationRegistry.getArguments().getString(USE_HOME_CMD, "false"));
+ mAppIdle =
+ Long.valueOf(
+ InstrumentationRegistry.getArguments()
+ .getString(
+ APP_IDLE_OPTION,
+ String.valueOf(TimeUnit.SECONDS.toMillis(0))));
//TODO(b/127356533): Choose a sensible default for app launch timeout after b/125356281.
mLaunchTimeout =
Long.valueOf(
@@ -115,12 +123,7 @@
String output = null;
try {
Log.i(LOG_TAG, String.format("Sending command to launch: %s", pkg));
- Intent intent =
- mInstrumentation
- .getContext()
- .getPackageManager()
- .getLaunchIntentForPackage(pkg);
- mInstrumentation.getContext().startActivity(intent);
+ mInstrumentation.getContext().startActivity(getOpenAppIntent());
} catch (ActivityNotFoundException e) {
removeDialogWatchers();
throw new TestHelperException(String.format("Failed to find package: %s", pkg), e);
@@ -143,6 +146,33 @@
pkg, System.currentTimeMillis() - launchInitiationTimeMs));
}
removeDialogWatchers();
+ // Idle for specified time after app launch
+ idleApp();
+ }
+
+ private void idleApp() {
+ if (mAppIdle != 0) {
+ Log.v(LOG_TAG, String.format("Idle app for %d ms", mAppIdle));
+ SystemClock.sleep(mAppIdle);
+ }
+ }
+
+ /**
+ * Returns the {@code Intent} used by {@code open()} to launch an {@code Activity}. The default
+ * implementation launches the default {@code Activity} of the package. Override this method to
+ * launch a different {@code Activity}.
+ */
+ public Intent getOpenAppIntent() {
+ Intent intent =
+ mInstrumentation
+ .getContext()
+ .getPackageManager()
+ .getLaunchIntentForPackage(getPackage());
+ if (intent == null) {
+ throw new IllegalStateException(
+ String.format("Failed to get intent of package: %s", getPackage()));
+ }
+ return intent;
}
/**
diff --git a/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
index b95baa9..c1f0c88 100644
--- a/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
+++ b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
@@ -20,38 +20,47 @@
/**
* A {@code HelperAccessor} can be included in any test to access an App Helper implementation.
*
- * <p>For example:
- * <code>
+ * <p>For example: <code>
* HelperAccessor<IXHelper> accessor = new HelperAccessor(IXHelper.class);
* accessor.get().performSomeAction();
- * </code>
+ * </code> To target a specific helper implementation by prefix, build this object and call, <code>
+ * withPrefix</code> on it.
*/
public class HelperAccessor<T extends IAppHelper> {
private final Class<T> mInterfaceClass;
+
private T mHelper;
+ private String mPrefix;
public HelperAccessor(Class<T> klass) {
mInterfaceClass = klass;
}
+ /** Selects only helpers that begin with the prefix, {@code prefix}. */
+ public HelperAccessor<T> withPrefix(String prefix) {
+ mPrefix = prefix;
+ // Unset the helper, in case this was changed after first use.
+ mHelper = null;
+ // Return self to follow a pseudo-builder initialization pattern.
+ return this;
+ }
+
public T get() {
if (mHelper == null) {
- mHelper = HelperManager.getInstance(
- InstrumentationRegistry.getContext(),
- InstrumentationRegistry.getInstrumentation())
- .get(mInterfaceClass);
+ if (mPrefix == null || mPrefix.isEmpty()) {
+ mHelper =
+ HelperManager.getInstance(
+ InstrumentationRegistry.getContext(),
+ InstrumentationRegistry.getInstrumentation())
+ .get(mInterfaceClass);
+ } else {
+ mHelper =
+ HelperManager.getInstance(
+ InstrumentationRegistry.getContext(),
+ InstrumentationRegistry.getInstrumentation())
+ .get(mInterfaceClass, mPrefix);
+ }
}
return mHelper;
}
-
- public T get(String prefix) {
- if (mHelper == null) {
- mHelper = HelperManager.getInstance(
- InstrumentationRegistry.getContext(),
- InstrumentationRegistry.getInstrumentation())
- .get(mInterfaceClass, prefix);
- }
- return mHelper;
- }
-
}
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
index 6882b92..2866fe5 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
@@ -26,7 +26,7 @@
/**
* Setup expectation: Date & time setting is open
*
- * Set the device date
+ * <p>Set the device date.
*
* @param date - input LocalDate object
*/
@@ -35,33 +35,44 @@
/**
* Setup expectation: Date & time setting is open
*
- * Get the current date displayed on the UI in LocalDate object
+ * <p>Get the current date displayed on the UI in LocalDate object.
*/
LocalDate getDate();
/**
* Setup expectation: Date & time setting is open
*
- * Set the device time
+ * <p>Set the device time in 12-hour format
*
* @param hour - input hour
* @param minute - input minute
- * @param AM_PM - input am/pm
+ * @param is_am - input am/pm
*/
- void setTime(int hour, int minute, boolean is_am);
+ void setTimeInTwelveHourFormat(int hour, int minute, boolean is_am);
/**
* Setup expectation: Date & time setting is open
*
- * Get the current time displayed on the UI
- * The return string format will match the UI format exactly
+ * <p>Set the device time in 24-hour format
+ *
+ * @param hour - input hour
+ * @param minute - input minute
+ */
+ void setTimeInTwentyFourHourFormat(int hour, int minute);
+
+ /**
+ * Setup expectation: Date & time setting is open
+ *
+ * <p>Get the current time displayed on the UI.
+ *
+ * @return returned time format will match the UI format exactly
*/
String getTime();
/**
* Setup expectation: Date & time setting is open
*
- * Set the device time zone
+ * <p>Set the device time zone.
*
* @param timezone - city selected for timezone
*/
@@ -70,21 +81,21 @@
/**
* Setup expectation: Date & time setting is open
*
- * Get the current timezone displayed on the UI
+ * <p>Get the current timezone displayed on the UI.
*/
String getTimeZone();
/**
* Setup expectation: Date & time setting is open
*
- * Check if the 24 hour format menu switch widget is toggoled on
+ * <p>Check if the 24 hour format is enabled
*/
- boolean isUseTwentyFourHourFormatSwitchWidgetOn();
+ boolean isTwentyFourHourFormatEnabled();
/**
* Setup expectation: Date & time setting is open
*
- * Toggle on/off 24 hour format widget switch
+ * <p>Toggle on/off 24 hour format widget switch.
*/
boolean toggleTwentyFourHourFormatSwitch();
}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
index 85b243d..15684fd 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
@@ -112,22 +112,22 @@
public void openPicture(int index);
/**
- * Setup expectations: Photos is open and a picture album is open.
+ * Setup expectations: Photos is open and a picture is open.
*
- * This method will scroll the picture album in the specified direction.
+ * <p>This method will scroll to next or previous picture in the specified direction.
*
* @param direction The direction to scroll, must be LEFT or RIGHT.
- * @return Returns whether album can be still scrolled in the given direction
+ * @return Returns whether picture can be still scrolled in the given direction
*/
- public boolean scrollAlbum(Direction direction);
+ public boolean scrollPicture(Direction direction);
/**
- * Setup expectations: Photos is open and a picture folder is open.
+ * Setup expectations: Photos is open and a page contains pictures or albums is open.
*
- * This method will scroll the Photos grid view in the specified direction.
+ * <p>This method will scroll the page in the specified direction.
*
* @param direction The direction of the scroll, must be UP or DOWN.
* @return Returns whether the object can still scroll in the given direction
*/
- public boolean scrollGridView(Direction direction);
+ public boolean scrollPage(Direction direction);
}
diff --git a/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java b/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
index b1e71dc..5c94b75 100644
--- a/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
+++ b/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
@@ -169,7 +169,7 @@
while ((nextCount = getNextCounter(reader)) > 0) {
totalCount += nextCount;
}
- result.put(currentProcess, totalCount);
+ result.put("binder_count_" + currentProcess, totalCount);
}
}
}
diff --git a/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java b/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
index aaa5d2d..b2c0948 100644
--- a/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
+++ b/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
@@ -37,7 +37,8 @@
private static final String LOG_TAG = SfStatsCollectionHelper.class.getSimpleName();
- private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^(\\w+)\\s+=\\s+(\\S+)");
+ private static final Pattern KEY_VALUE_PATTERN =
+ Pattern.compile("^(\\w+)\\s+=\\s+(\\d+\\.?\\d*|.*).*");
private static final Pattern HISTOGRAM_PATTERN =
Pattern.compile("([^\\n]+)\\n((\\d+ms=\\d+\\s+)+)");
diff --git a/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java b/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
index ca0f1df..8358dec 100644
--- a/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
+++ b/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
@@ -100,7 +100,7 @@
mHelper.addTrackedProcesses(process);
Map<String, Integer> result = new HashMap<>();
mHelper.parseMetrics(getBufferedReader(), result);
- assertTrue(result.get(process) == 0);
+ assertTrue(result.get("binder_count_" + process) == 0);
}
@Test
@@ -109,7 +109,7 @@
mHelper.addTrackedProcesses(process);
Map<String, Integer> result = new HashMap<>();
mHelper.parseMetrics(getBufferedReader(), result);
- assertTrue(result.get(process) == 20);
+ assertTrue(result.get("binder_count_" + process) == 20);
}
private BufferedReader getBufferedReader() throws Exception {
diff --git a/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java b/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
index 3d851ea..c986a67 100644
--- a/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
+++ b/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
@@ -85,7 +85,25 @@
+ "post2present histogram is as below:\n"
+ "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0\n"
+ "post2acquire histogram is as below:\n"
- + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0";
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0\n"
+ + "\n"
+ + "layerName = SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0\n"
+ + "packageName = \n"
+ + "totalFrames = 2352\n"
+ + "droppedFrames = 0\n"
+ + "averageFPS = 59.999\n"
+ + "present2present histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+ + "latch2present histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+ + "desired2present histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+ + "acquire2present histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+ + "post2present histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+ + "post2acquire histogram is as below:\n"
+ + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0";
private static final String LOG_TAG = SfStatsCollectionHelperTest.class.getSimpleName();
@@ -140,6 +158,27 @@
constructKey(
SFSTATS_METRICS_PREFIX,
"GLOBAL",
+ "clientCompositionFrames".toUpperCase())))
+ .isEqualTo(Double.valueOf(0));
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "GLOBAL",
+ "displayOnTime".toUpperCase())))
+ .isEqualTo(Double.valueOf(2485421));
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "GLOBAL",
+ "totalP2PTime".toUpperCase())))
+ .isEqualTo(Double.valueOf(2674034));
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "GLOBAL",
"FRAME_CPU_DURATION_AVG")))
.isEqualTo(Double.valueOf(5.5));
assertThat(
@@ -191,6 +230,27 @@
"com.google.android.nexuslauncher.NexusLauncherActivity#0",
"AVERAGE_FPS")))
.isEqualTo(84.318);
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+ "TOTAL_FRAMES")))
+ .isEqualTo(Double.valueOf(2352));
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+ "DROPPED_FRAMES")))
+ .isEqualTo(Double.valueOf(0));
+ assertThat(
+ metrics.get(
+ constructKey(
+ SFSTATS_METRICS_PREFIX,
+ "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+ "AVERAGE_FPS")))
+ .isEqualTo(59.999);
mHelper.stopCollecting();
}
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
index da0ff2c..b2f1f8c 100644
--- a/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
@@ -50,6 +50,7 @@
private static final String PROC_MEMINFO = "cat /proc/meminfo";
private static final String LINE_SEPARATOR = "\\n";
private static final String MEM_AVAILABLE_PATTERN = "^MemAvailable.*";
+ private static final String MEM_FREE_PATTERN = "^MemFree.*";
private static final Pattern CACHE_PROC_START_PATTERN = Pattern.compile(".*: Cached$");
private static final Pattern PID_PATTERN = Pattern.compile("^.*pid(?<processid> [0-9]*).*$");
private static final String DUMPSYS_PROCESS = "dumpsys meminfo %s";
@@ -57,6 +58,7 @@
private static final String PROCESS_ID = "processid";
public static final String MEM_AVAILABLE_CACHE_PROC_DIRTY = "MemAvailable_CacheProcDirty_bytes";
public static final String PROC_MEMINFO_MEM_AVAILABLE= "proc_meminfo_memavailable_bytes";
+ public static final String PROC_MEMINFO_MEM_FREE= "proc_meminfo_memfree_bytes";
public static final String DUMPSYS_CACHED_PROC_MEMORY= "dumpsys_cached_procs_memory_bytes";
private UiDevice mUiDevice;
@@ -77,27 +79,35 @@
String memInfo;
try {
memInfo = mUiDevice.executeShellCommand(PROC_MEMINFO);
+ Log.i(TAG, "cat proc/meminfo :" + memInfo);
} catch (IOException ioe) {
Log.e(TAG, "Failed to read " + PROC_MEMINFO + ".", ioe);
return null;
}
Pattern memAvailablePattern = Pattern.compile(MEM_AVAILABLE_PATTERN, Pattern.MULTILINE);
+ Pattern memFreePattern = Pattern.compile(MEM_FREE_PATTERN, Pattern.MULTILINE);
Matcher memAvailableMatcher = memAvailablePattern.matcher(memInfo);
+ Matcher memFreeMatcher = memFreePattern.matcher(memInfo);
String[] memAvailable = null;
- if (memAvailableMatcher.find()) {
+ String[] memFree = null;
+ if (memAvailableMatcher.find() && memFreeMatcher.find()) {
memAvailable = memAvailableMatcher.group(0).split(SEPARATOR);
+ memFree = memFreeMatcher.group(0).split(SEPARATOR);
}
- if (memAvailable == null) {
- Log.e(TAG, "MemAvailable is null.");
+ if (memAvailable == null || memFree == null) {
+ Log.e(TAG, "MemAvailable or MemFree is null.");
return null;
}
Map<String, Long> results = new HashMap<>();
long memAvailableProc = Long.parseLong(memAvailable[1]);
results.put(PROC_MEMINFO_MEM_AVAILABLE, (memAvailableProc * 1024));
+ long memFreeProc = Long.parseLong(memFree[1]);
+ results.put(PROC_MEMINFO_MEM_FREE, (memFreeProc * 1024));
+
long cacheProcDirty = memAvailableProc;
byte[] dumpsysMemInfoBytes = MetricUtility.executeCommandBlocking(DUMPSYS_MEMIFNO,
InstrumentationRegistry.getInstrumentation());
@@ -107,7 +117,7 @@
for (String process : cachedProcList) {
Log.i(TAG, "Cached Process" + process);
Matcher match;
- if (((match = matches(PID_PATTERN, process))) != null) {
+ if ((match = matches(PID_PATTERN, process)) != null) {
String processId = match.group(PROCESS_ID);
String processDumpSysMemInfo = String.format(DUMPSYS_PROCESS, processId);
String processInfoStr;
@@ -173,7 +183,7 @@
Log.i(TAG, currLine);
Matcher match;
if (!isCacheProcSection
- && ((match = matches(CACHE_PROC_START_PATTERN, currLine))) == null) {
+ && (match = matches(CACHE_PROC_START_PATTERN, currLine)) == null) {
// Continue untill the start of cache proc section.
continue;
} else {
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java
deleted file mode 100644
index f73ee53..0000000
--- a/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2019 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.helpers;
-
-import static com.android.helpers.MetricUtility.constructKey;
-
-import android.icu.text.NumberFormat;
-import android.support.test.uiautomator.UiDevice;
-import android.util.Log;
-import androidx.test.InstrumentationRegistry;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.text.ParseException;
-import java.util.HashMap;
-import java.util.InputMismatchException;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.UUID;
-
-/**
- * Helper to collect rss snapshot for a list of processes.
- */
-public class RssSnapshotHelper implements ICollectorHelper<String> {
- private static final String TAG = RssSnapshotHelper.class.getSimpleName();
-
- private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
- private static final String PIDOF_CMD = "pidof %s";
- private static final String SHOWMAP_CMD = "showmap -v %d";
-
- public static final String RSS_METRIC_PREFIX = "showmap_rss_bytes";
- public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
-
- private String[] mProcessNames = null;
- private String mTestOutputDir = null;
- private String mTestOutputFile = null;
-
- private int mDropCacheOption;
- private UiDevice mUiDevice;
-
- // Map to maintain per-process rss.
- private Map<String, String> mRssMap = new HashMap<>();
-
- public void setUp(String testOutputDir, String... processNames) {
- mProcessNames = processNames;
- mTestOutputDir = testOutputDir;
- mDropCacheOption = 0;
- mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- }
-
- @Override
- public boolean startCollecting() {
- if (mTestOutputDir == null || mProcessNames == null) {
- Log.e(TAG, String.format("Invalid test setup"));
- return false;
- }
-
- File directory = new File(mTestOutputDir);
- String filePath =
- String.format("%s/rss_snapshot%d.txt", mTestOutputDir, UUID.randomUUID().hashCode());
- File file = new File(filePath);
-
- // Make sure directory exists and file does not
- if (directory.exists()) {
- if (file.exists() && !file.delete()) {
- Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
- return false;
- }
- } else {
- if (!directory.mkdirs()) {
- Log.e(TAG, String.format("Failed to create result output directory %s", mTestOutputDir));
- return false;
- }
- }
-
- // Create an empty file to fail early in case there are no write permissions
- try {
- if (!file.createNewFile()) {
- // This should not happen unless someone created the file right after we deleted it
- Log.e(TAG, String.format("Race with another user of result output file %s", filePath));
- return false;
- }
- } catch (IOException e) {
- Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
- return false;
- }
-
- mTestOutputFile = filePath;
- return true;
- }
-
- @Override
- public Map<String, String> getMetrics() {
- try {
- // Drop cache if requested
- if (mDropCacheOption > 0) {
- dropCache(mDropCacheOption);
- }
-
- if (mProcessNames.length == 0) {
- // No processes specified, just return
- return mRssMap;
- }
-
- FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
- for (String processName : mProcessNames) {
- long pid, rss;
- String showmapOutput;
-
- // Collect required data
- try {
- pid = getPid(processName);
- showmapOutput = execShowMap(processName, pid);
- rss = extractTotalRss(processName, showmapOutput);
- } catch (RuntimeException e) {
- Log.e(TAG, e.getMessage(), e.getCause());
- // Skip this process and continue with the next one
- continue;
- }
-
- // Store showmap output into file
- storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
-
- // Store metrics
- mRssMap.put(constructKey(RSS_METRIC_PREFIX, processName), Long.toString(rss * 1024));
- }
- writer.close();
- mRssMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
- } catch (RuntimeException e) {
- Log.e(TAG, e.getMessage(), e.getCause());
- } catch (IOException e) {
- Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
- }
-
- return mRssMap;
- }
-
- @Override
- public boolean stopCollecting() {
- return true;
- }
-
- /**
- * Set drop cache option.
- *
- * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
- * @return true on success, false if input option is invalid
- */
- public boolean setDropCacheOption(int dropCacheOption) {
- // Valid values are 1..3
- if (dropCacheOption < 1 || dropCacheOption > 3) {
- return false;
- }
-
- mDropCacheOption = dropCacheOption;
- return true;
- }
-
- /**
- * Drops kernel memory cache.
- *
- * @param cacheOption drop pagecache (1), slab (2) or all (3) caches
- */
- private void dropCache(int cacheOption) throws RuntimeException {
- try {
- mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
- } catch (IOException e) {
- throw new RuntimeException("Unable to drop caches", e);
- }
- }
-
- /**
- * Get pid of the process with {@code processName} name.
- *
- * @param processName name of the process to get pid
- * @return pid of the specified process
- */
- private int getPid(String processName) throws RuntimeException {
- try {
- // Note that only the first pid returned by "pidof" will be used.
- String pidofOutput = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
- return NumberFormat.getInstance().parse(pidofOutput).intValue();
- } catch (IOException | ParseException e) {
- throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
- }
- }
-
- /**
- * Executes showmap command for the process with {@code processName} name and {@code pid} pid.
- *
- * @param processName name of the process to run showmap for
- * @param pid pid of the process to run showmap for
- * @return the output of showmap command
- */
- private String execShowMap(String processName, long pid) throws IOException {
- try {
- return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
- } catch (IOException e) {
- throw new RuntimeException(
- String.format("Unable to execute showmap command for %s ", processName), e);
- }
- }
-
- /**
- * Extract total RSS from showmap command output for the process with {@code processName} name.
- *
- * @param processName name of the process to extract RSS for
- * @param showmapOutput showmap command output
- * @return total RSS of the process
- */
- private long extractTotalRss(String processName, String showmapOutput) throws RuntimeException {
- try {
- int pos = showmapOutput.lastIndexOf("----");
- Scanner sc = new Scanner(showmapOutput.substring(pos));
- sc.next();
- sc.nextLong();
- return sc.nextLong();
- } catch (IndexOutOfBoundsException | InputMismatchException e) {
- throw new RuntimeException(
- String.format("Unexpected showmap format for %s ", processName), e);
- }
- }
-
- /**
- * Store test results for one process into file.
- *
- * @param fileName name of the file being written
- * @param processName name of the process
- * @param pid pid of the process
- * @param showmapOutput showmap command output
- * @param writer file writer to write the data
- */
- private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
- FileWriter writer) throws RuntimeException {
- try {
- writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
- writer.write(showmapOutput);
- writer.write('\n');
- } catch (IOException e) {
- throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
- }
- }
-}
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
new file mode 100644
index 0000000..fba95b0
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2019 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.helpers;
+
+import static com.android.helpers.MetricUtility.constructKey;
+
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.InputMismatchException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Helper to collect memory information for a list of processes from showmap.
+ */
+public class ShowmapSnapshotHelper implements ICollectorHelper<String> {
+ private static final String TAG = ShowmapSnapshotHelper.class.getSimpleName();
+
+ private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
+ private static final String PIDOF_CMD = "pidof %s";
+ public static final String ALL_PROCESSES_CMD = "ps -A";
+ private static final String SHOWMAP_CMD = "showmap -v %d";
+
+ public static final String OUTPUT_METRIC_PATTERN = "showmap_%s_bytes";
+ public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
+ public static final String PROCESS_COUNT = "process_count";
+
+ private String[] mProcessNames = null;
+ private String mTestOutputDir = null;
+ private String mTestOutputFile = null;
+
+ private int mDropCacheOption;
+ private boolean mCollectForAllProcesses = false;
+ private UiDevice mUiDevice;
+
+ // Map to maintain per-process memory info
+ private Map<String, String> mMemoryMap = new HashMap<>();
+
+ // Maintain metric name and the index it corresponds to in the showmap output
+ // summary
+ private Map<Integer, String> mMetricNameIndexMap = new HashMap<>();
+
+ public void setUp(String testOutputDir, String... processNames) {
+ mProcessNames = processNames;
+ mTestOutputDir = testOutputDir;
+ mDropCacheOption = 0;
+ mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+
+ @Override
+ public boolean startCollecting() {
+ if (mTestOutputDir == null) {
+ Log.e(TAG, String.format("Invalid test setup"));
+ return false;
+ }
+
+ File directory = new File(mTestOutputDir);
+ String filePath =
+ String.format("%s/showmap_snapshot%d.txt", mTestOutputDir, UUID.randomUUID().hashCode());
+ File file = new File(filePath);
+
+ // Make sure directory exists and file does not
+ if (directory.exists()) {
+ if (file.exists() && !file.delete()) {
+ Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
+ return false;
+ }
+ } else {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, String.format("Failed to create result output directory %s", mTestOutputDir));
+ return false;
+ }
+ }
+
+ // Create an empty file to fail early in case there are no write permissions
+ try {
+ if (!file.createNewFile()) {
+ // This should not happen unless someone created the file right after we deleted it
+ Log.e(TAG, String.format("Race with another user of result output file %s", filePath));
+ return false;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
+ return false;
+ }
+
+ mTestOutputFile = filePath;
+ return true;
+ }
+
+ @Override
+ public Map<String, String> getMetrics() {
+ try {
+ // Drop cache if requested
+ if (mDropCacheOption > 0) {
+ dropCache(mDropCacheOption);
+ }
+
+ if (mCollectForAllProcesses) {
+ Log.i(TAG, "Collecting memory metrics for all processes.");
+ mProcessNames = getAllProcessNames();
+ } else if (mProcessNames.length > 0) {
+ Log.i(TAG, "Collecting memory only for given list of process");
+ } else if (mProcessNames.length == 0) {
+ // No processes specified, just return empty map
+ return mMemoryMap;
+ }
+
+ FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
+ for (String processName : mProcessNames) {
+ List<Integer> pids = new ArrayList<>();
+
+ // Collect required data
+ try {
+ pids = getPids(processName);
+ for (Integer pid: pids) {
+ String showmapOutput = execShowMap(processName, pid);
+ parseAndUpdateMemoryInfo(processName, showmapOutput);
+ // Store showmap output into file. If there are more than one process
+ // with same name write the individual showmap associated with pid.
+ storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, e.getMessage(), e.getCause());
+ // Skip this process and continue with the next one
+ continue;
+ }
+ }
+ // Store the unique process count. -1 to exclude the "ps" process name.
+ mMemoryMap.put(PROCESS_COUNT, Integer.toString(mProcessNames.length - 1));
+ writer.close();
+ mMemoryMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
+ } catch (RuntimeException e) {
+ Log.e(TAG, e.getMessage(), e.getCause());
+ } catch (IOException e) {
+ Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
+ }
+
+ return mMemoryMap;
+ }
+
+ @Override
+ public boolean stopCollecting() {
+ return true;
+ }
+
+ /**
+ * Set drop cache option.
+ *
+ * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
+ * @return true on success, false if input option is invalid
+ */
+ public boolean setDropCacheOption(int dropCacheOption) {
+ // Valid values are 1..3
+ if (dropCacheOption < 1 || dropCacheOption > 3) {
+ return false;
+ }
+
+ mDropCacheOption = dropCacheOption;
+ return true;
+ }
+
+ /**
+ * Drops kernel memory cache.
+ *
+ * @param cacheOption drop pagecache (1), slab (2) or all (3) caches
+ */
+ private void dropCache(int cacheOption) throws RuntimeException {
+ try {
+ mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to drop caches", e);
+ }
+ }
+
+ /**
+ * Get pid's of the process with {@code processName} name.
+ *
+ * @param processName name of the process to get pid
+ * @return pid's of the specified process
+ */
+ private List<Integer> getPids(String processName) throws RuntimeException {
+ try {
+ String pidofOutput = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
+
+ // Sample output for the process with more than 1 pid.
+ // Sample command : "pidof init"
+ // Sample output : 1 559
+ String[] pids = pidofOutput.split("\\s+");
+ List<Integer> pidList = new ArrayList<>();
+ for (String pid: pids) {
+ pidList.add(Integer.parseInt(pid.trim()));
+ }
+ return pidList;
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
+ }
+ }
+
+ /**
+ * Executes showmap command for the process with {@code processName} name and {@code pid} pid.
+ *
+ * @param processName name of the process to run showmap for
+ * @param pid pid of the process to run showmap for
+ * @return the output of showmap command
+ */
+ private String execShowMap(String processName, long pid) throws IOException {
+ try {
+ return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
+ } catch (IOException e) {
+ throw new RuntimeException(
+ String.format("Unable to execute showmap command for %s ", processName), e);
+ }
+ }
+
+ /**
+ * Extract memory metrics from showmap command output for the process with {@code processName}
+ * name.
+ *
+ * @param processName name of the process to extract memory info for
+ * @param showmapOutput showmap command output
+ */
+ private void parseAndUpdateMemoryInfo(String processName, String showmapOutput)
+ throws RuntimeException {
+ try {
+
+ // -------- -------- -------- -------- -------- -------- -------- -------- ----- ------ ----
+ // virtual shared shared private private
+ // size RSS PSS clean dirty clean dirty swap swapPSS flags object
+ // ------- -------- -------- -------- -------- -------- -------- -------- ------ ----- ----
+ //10810272 5400 1585 3800 168 264 1168 0 0 TOTAL
+
+ int pos = showmapOutput.lastIndexOf("----");
+ String summarySplit[] = showmapOutput.substring(pos).trim().split("\\s+");
+
+ for (Map.Entry<Integer, String> entry : mMetricNameIndexMap.entrySet()) {
+ String metricKey = constructKey(String.format(OUTPUT_METRIC_PATTERN, entry.getValue()),
+ processName);
+ // If there are multiple pids associated with the process name then update the
+ // existing entry in the map otherwise add new entry in the map.
+ if(mMemoryMap.containsKey(metricKey)) {
+ long currValue = Long.parseLong(mMemoryMap.get(metricKey));
+ mMemoryMap.put(metricKey, Long.toString(currValue +
+ (Long.parseLong(summarySplit[entry.getKey() + 1]) * 1024)));
+ } else {
+ mMemoryMap.put(metricKey, Long.toString(Long.parseLong(
+ summarySplit[entry.getKey() + 1]) * 1024));
+ }
+ }
+ } catch (IndexOutOfBoundsException | InputMismatchException e) {
+ throw new RuntimeException(
+ String.format("Unexpected showmap format for %s ", processName), e);
+ }
+ }
+
+ /**
+ * Store test results for one process into file.
+ *
+ * @param fileName name of the file being written
+ * @param processName name of the process
+ * @param pid pid of the process
+ * @param showmapOutput showmap command output
+ * @param writer file writer to write the data
+ */
+ private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
+ FileWriter writer) throws RuntimeException {
+ try {
+ writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
+ writer.write(showmapOutput);
+ writer.write('\n');
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
+ }
+ }
+
+ /**
+ * Set the memory metric name and corresponding index to parse from the showmap output summary.
+ * @param metricNameIndexStr comma separated metric_name:index
+ *
+ * TODO: Pre-process the string into map and pass the map to this method.
+ */
+ public void setMetricNameIndex(String metricNameIndexStr) {
+ Log.i(TAG, String.format("Metric Name index %s", metricNameIndexStr));
+ String metricDetails[] = metricNameIndexStr.split(",");
+ for (String metricDetail : metricDetails) {
+ String metricDetailsSplit[] = metricDetail.split(":");
+ if (metricDetailsSplit.length == 2) {
+ mMetricNameIndexMap.put(Integer.parseInt(
+ metricDetailsSplit[1]), metricDetailsSplit[0]);
+ }
+ }
+ Log.i(TAG, String.format("Metric Name index map size %s", mMetricNameIndexMap.size()));
+ }
+
+ /**
+ * Enables memory collection for all processes.
+ */
+ public void setAllProcesses() {
+ mCollectForAllProcesses = true;
+ }
+
+ /**
+ * Get all process names running in the system.
+ */
+ private String[] getAllProcessNames() {
+ Set<String> allProcessNames = new LinkedHashSet<>();
+ try {
+ String psOutput = mUiDevice.executeShellCommand(ALL_PROCESSES_CMD);
+ // Split the lines
+ String allProcesses[] = psOutput.split("\\n");
+ for (String invidualProcessDetails : allProcesses) {
+ Log.i(TAG, String.format("Process detail: %s", invidualProcessDetails));
+ // Sample process detail line
+ // system 603 1 41532 5396 SyS_epoll+ 0 S servicemanager
+ String processSplit[] = invidualProcessDetails.split("\\s+");
+ // Parse process name
+ String processName = processSplit[processSplit.length - 1].trim();
+ // Include the process name which are not enclosed in [].
+ if (!processName.startsWith("[") && !processName.endsWith("]")) {
+ // Skip the first (i.e header) line from "ps -A" output.
+ if (processName.equalsIgnoreCase("NAME")) {
+ continue;
+ }
+ Log.i(TAG, String.format("Including the process %s", processName));
+ allProcessNames.add(processName);
+ }
+ }
+ } catch (IOException ioe) {
+ throw new RuntimeException(
+ String.format("Unable execute all processes command %s ", ALL_PROCESSES_CMD),
+ ioe);
+ }
+ return allProcessNames.toArray(new String[0]);
+ }
+}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
index 283ab36..9770908 100644
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
@@ -54,6 +54,7 @@
assertTrue(freeMemMetrics.containsKey(FreeMemHelper.DUMPSYS_CACHED_PROC_MEMORY));
assertTrue(freeMemMetrics.get(FreeMemHelper.MEM_AVAILABLE_CACHE_PROC_DIRTY) > 0);
assertTrue(freeMemMetrics.get(FreeMemHelper.PROC_MEMINFO_MEM_AVAILABLE) > 0);
+ assertTrue(freeMemMetrics.get(FreeMemHelper.PROC_MEMINFO_MEM_FREE) > 0);
assertTrue(freeMemMetrics.get(FreeMemHelper.DUMPSYS_CACHED_PROC_MEMORY) > 0);
}
}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java
deleted file mode 100644
index 33e49cc..0000000
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 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.helpers.tests;
-
-import static com.android.helpers.MetricUtility.constructKey;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import androidx.test.runner.AndroidJUnit4;
-import com.android.helpers.RssSnapshotHelper;
-import java.util.Map;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Android Unit tests for {@link RssSnapshotHelper}.
- *
- * To run:
- * atest CollectorsHelperTest:com.android.helpers.tests.RssSnapshotHelperTest
- */
-@RunWith(AndroidJUnit4.class)
-public class RssSnapshotHelperTest {
- private static final String TAG = RssSnapshotHelperTest.class.getSimpleName();
-
- // Valid output file
- private static final String VALID_OUTPUT_DIR = "/sdcard/test_results";
- // Invalid output file (no permissions to write)
- private static final String INVALID_OUTPUT_DIR = "/data/local/tmp";
-
- // Lists of process names
- private static final String[] EMPTY_PROCESS_LIST = {};
- private static final String[] ONE_PROCESS_LIST = {"com.android.systemui"};
- private static final String[] TWO_PROCESS_LIST = {"com.android.systemui", "system_server"};
-
- private RssSnapshotHelper mRssSnapshotHelper;
-
- @Before
- public void setUp() {
- mRssSnapshotHelper = new RssSnapshotHelper();
- }
-
- /**
- * Test start collecting returns false if the helper has not been properly set up.
- */
- @Test
- public void testSetUpNotCalled() {
- assertFalse(mRssSnapshotHelper.startCollecting());
- }
-
- /**
- * Test invalid options for drop cache flag.
- */
- @Test
- public void testInvalidDropCacheOptions() {
- assertFalse(mRssSnapshotHelper.setDropCacheOption(-1));
- assertFalse(mRssSnapshotHelper.setDropCacheOption(0));
- assertFalse(mRssSnapshotHelper.setDropCacheOption(4));
- }
-
- /**
- * Test invalid options for drop cache flag.
- */
- @Test
- public void testValidDropCacheOptions() {
- assertTrue(mRssSnapshotHelper.setDropCacheOption(1));
- assertTrue(mRssSnapshotHelper.setDropCacheOption(2));
- assertTrue(mRssSnapshotHelper.setDropCacheOption(3));
- }
-
- /**
- * Test no metrics are sampled if process name is empty.
- */
- @Test
- public void testEmptyProcessName() {
- mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, EMPTY_PROCESS_LIST);
- Map<String, String> metrics = mRssSnapshotHelper.getMetrics();
- assertTrue(metrics.isEmpty());
- }
-
- /**
- * Test sampling on a valid and running process.
- */
- @Test
- public void testValidFile() {
- mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, ONE_PROCESS_LIST);
- assertTrue(mRssSnapshotHelper.startCollecting());
- }
-
- /**
- * Test sampling on using an invalid output file.
- */
- @Test
- public void testInvalidFile() {
- mRssSnapshotHelper.setUp(INVALID_OUTPUT_DIR, ONE_PROCESS_LIST);
- assertFalse(mRssSnapshotHelper.startCollecting());
- }
-
- /**
- * Test getting metrics from one process.
- */
- @Test
- public void testGetMetrics_OneProcess() {
- testProcessList(ONE_PROCESS_LIST);
- }
-
- /**
- * Test getting metrics from multiple processes process.
- */
- @Test
- public void testGetMetrics_MultipleProcesses() {
- testProcessList(TWO_PROCESS_LIST);
- }
-
- private void testProcessList(String... processNames) {
- mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, processNames);
- assertTrue(mRssSnapshotHelper.startCollecting());
- Map<String, String> metrics = mRssSnapshotHelper.getMetrics();
- assertFalse(metrics.isEmpty());
- for (String processName : processNames) {
- assertTrue(
- metrics.containsKey(constructKey(RssSnapshotHelper.RSS_METRIC_PREFIX, processName)));
- }
- assertTrue(metrics.containsKey(RssSnapshotHelper.OUTPUT_FILE_PATH_KEY));
- }
-}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
new file mode 100644
index 0000000..ca1218a
--- /dev/null
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.helpers.tests;
+
+import static com.android.helpers.MetricUtility.constructKey;
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+import com.android.helpers.ShowmapSnapshotHelper;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Android Unit tests for {@link ShowmapSnapshotHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.ShowmapSnapshotHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ShowmapSnapshotHelperTest {
+ private static final String TAG = ShowmapSnapshotHelperTest.class.getSimpleName();
+
+ // Valid output file
+ private static final String VALID_OUTPUT_DIR = "/sdcard/test_results";
+ // Invalid output file (no permissions to write)
+ private static final String INVALID_OUTPUT_DIR = "/data/local/tmp";
+ // Valid metric index string.
+ private static final String METRIC_INDEX_STR = "rss:1,pss:2";
+ // Invalid metric index string. Reverse order.
+ private static final String METRIC_INVALID_INDEX_STR = "1:pss";
+ // Empty metric index string.
+ private static final String METRIC_EMPTY_INDEX_STR = "";
+
+ // Lists of process names
+ private static final String[] EMPTY_PROCESS_LIST = {};
+ private static final String[] ONE_PROCESS_LIST = {"com.android.systemui"};
+ private static final String[] TWO_PROCESS_LIST = {"com.android.systemui", "system_server"};
+ private static final String[] NO_PROCESS_LIST = {null};
+
+ private ShowmapSnapshotHelper mShowmapSnapshotHelper;
+
+ @Before
+ public void setUp() {
+ mShowmapSnapshotHelper = new ShowmapSnapshotHelper();
+ }
+
+ /**
+ * Test start collecting returns false if the helper has not been properly set up.
+ */
+ @Test
+ public void testSetUpNotCalled() {
+ assertFalse(mShowmapSnapshotHelper.startCollecting());
+ }
+
+ /**
+ * Test invalid options for drop cache flag.
+ */
+ @Test
+ public void testInvalidDropCacheOptions() {
+ assertFalse(mShowmapSnapshotHelper.setDropCacheOption(-1));
+ assertFalse(mShowmapSnapshotHelper.setDropCacheOption(0));
+ assertFalse(mShowmapSnapshotHelper.setDropCacheOption(4));
+ }
+
+ /**
+ * Test invalid options for drop cache flag.
+ */
+ @Test
+ public void testValidDropCacheOptions() {
+ assertTrue(mShowmapSnapshotHelper.setDropCacheOption(1));
+ assertTrue(mShowmapSnapshotHelper.setDropCacheOption(2));
+ assertTrue(mShowmapSnapshotHelper.setDropCacheOption(3));
+ }
+
+ /**
+ * Test no metrics are sampled if process name is empty.
+ */
+ @Test
+ public void testEmptyProcessName() {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, EMPTY_PROCESS_LIST);
+ Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+ assertTrue(metrics.isEmpty());
+ }
+
+ /**
+ * Test sampling on a valid and running process.
+ */
+ @Test
+ public void testValidFile() {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, ONE_PROCESS_LIST);
+ assertTrue(mShowmapSnapshotHelper.startCollecting());
+ }
+
+ /**
+ * Test sampling on using an invalid output file.
+ */
+ @Test
+ public void testInvalidFile() {
+ mShowmapSnapshotHelper.setUp(INVALID_OUTPUT_DIR, ONE_PROCESS_LIST);
+ assertFalse(mShowmapSnapshotHelper.startCollecting());
+ }
+
+ /**
+ * Test getting metrics from one process.
+ */
+ @Test
+ public void testGetMetrics_OneProcess() {
+ testProcessList(METRIC_INDEX_STR, ONE_PROCESS_LIST);
+ }
+
+ /**
+ * Test getting metrics from multiple processes process.
+ */
+ @Test
+ public void testGetMetrics_MultipleProcesses() {
+ testProcessList(METRIC_INDEX_STR, TWO_PROCESS_LIST);
+ }
+
+ /**
+ * Test all process flag return more than 2 processes metrics atleast.
+ */
+ @Test
+ public void testGetMetrics_AllProcess() {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+ mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INDEX_STR);
+ mShowmapSnapshotHelper.setAllProcesses();
+ assertTrue(mShowmapSnapshotHelper.startCollecting());
+ Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+ assertTrue(metrics.size() > 2);
+ assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+
+ }
+
+ @Test
+ public void testGetMetrics_Invalid_Metric_Pattern() {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+ try {
+ mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INVALID_INDEX_STR);
+ fail("Should have thrown an exception due to invalid pattern.");
+ } catch (Exception e) {
+ // No-op during the exception
+ }
+
+ mShowmapSnapshotHelper.setAllProcesses();
+ assertTrue(mShowmapSnapshotHelper.startCollecting());
+ Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+ // process count and path to snapshot file in the output by default.
+ assertTrue(metrics.size() == 2);
+ }
+
+ @Test
+ public void testGetMetrics_Empty_Metric_Pattern() {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+ mShowmapSnapshotHelper.setMetricNameIndex(METRIC_EMPTY_INDEX_STR);
+
+ mShowmapSnapshotHelper.setAllProcesses();
+ assertTrue(mShowmapSnapshotHelper.startCollecting());
+ Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+ // process count and path to snapshot file in the output by default.
+ assertTrue(metrics.size() == 2);
+ }
+
+
+ private void testProcessList(String metricIndexStr, String... processNames) {
+ mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, processNames);
+ mShowmapSnapshotHelper.setMetricNameIndex(metricIndexStr);
+ assertTrue(mShowmapSnapshotHelper.startCollecting());
+ Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+ assertFalse(metrics.isEmpty());
+ for (String processName : processNames) {
+ assertTrue(
+ metrics.containsKey(constructKey(String.format(
+ ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "rss"), processName)));
+ assertTrue(
+ metrics.containsKey(constructKey(String.format(
+ ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "pss"), processName)));
+ }
+ assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+ }
+}
diff --git a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
index abf92e2..c2ddb17 100644
--- a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
+++ b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
@@ -149,7 +149,7 @@
*
* @return true if perfetto is stopped successfully.
*/
- private boolean stopPerfetto() throws IOException {
+ public boolean stopPerfetto() throws IOException {
String stopOutput = mUIDevice.executeShellCommand(PERFETTO_STOP_CMD);
Log.i(LOG_TAG, String.format("Perfetto stop command output - %s", stopOutput));
int waitCount = 0;
diff --git a/libraries/collectors-helper/tests/Android.bp b/libraries/collectors-helper/tests/Android.bp
new file mode 100644
index 0000000..50e844c
--- /dev/null
+++ b/libraries/collectors-helper/tests/Android.bp
@@ -0,0 +1,27 @@
+// 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.
+
+android_test {
+ name: "CollectorsHelperAospTest",
+ defaults: ["tradefed_errorprone_defaults"],
+
+ static_libs: [
+ "perfetto-helper-test",
+ "jank-helper-test",
+ "memory-helper-test",
+ "system-helper-test",
+ ],
+
+ sdk_version: "current",
+}
diff --git a/libraries/collectors-helper/tests/AndroidManifest.xml b/libraries/collectors-helper/tests/AndroidManifest.xml
new file mode 100644
index 0000000..bfcba7b
--- /dev/null
+++ b/libraries/collectors-helper/tests/AndroidManifest.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.helpers.tests" >
+ <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="24" />
+ <uses-permission android:name="android.permission.DUMP" />
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.helpers.tests"
+ android:label="Collector Helper Tests" />
+</manifest>
diff --git a/libraries/collectors-helper/tests/AndroidTest.xml b/libraries/collectors-helper/tests/AndroidTest.xml
new file mode 100644
index 0000000..3a1e771
--- /dev/null
+++ b/libraries/collectors-helper/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<configuration description="Configuration for AOSP platform collectors helper tests.">
+ <option name="test-suite-tag" value="collectors_helper_aosp" />
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CollectorsHelperAospTest.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.helpers.tests" />
+ </test>
+</configuration>
+
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
index cac6157..eb83858 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
@@ -70,6 +70,9 @@
// Default collect iteration interval.
private static final int DEFAULT_COLLECT_INTERVAL = 1;
+ // Default skip metric until iteration count.
+ private static final int SKIP_UNTIL_DEFAULT_ITERATION = 0;
+
/** Options keys that the collector can receive. */
// Filter groups, comma separated list of group name to be included or excluded
public static final String INCLUDE_FILTER_GROUP_KEY = "include-filter-group";
@@ -79,6 +82,11 @@
// Collect metric every nth iteration of a test with the same name.
public static final String COLLECT_ITERATION_INTERVAL = "collect_iteration_interval";
+ // Skip metric collection until given n iteration. Uses 1 indexing here.
+ // For example if overall iteration is 10 and skip until iteration is set
+ // to 3. Metric will not be collected for 1st,2nd and 3rd iteration.
+ public static final String SKIP_METRIC_UNTIL_ITERATION = "skip_metric_until_iteration";
+
private static final String NAMESPACE_SEPARATOR = ":";
private DataRecord mRunData;
@@ -91,6 +99,7 @@
// Store the method name and invocation count.
private Map<String, Integer> mTestIdInvocationCount = new HashMap<>();
private int mCollectIterationInterval = 1;
+ private int mSkipMetricUntilIteration = 0;
public BaseMetricListener() {
mIncludeFilters = new ArrayList<>();
@@ -137,8 +146,12 @@
@Override
public final void testStarted(Description description) throws Exception {
+
+ // Update the current invocation before proceeding with metric collection.
+ // mTestIdInvocationCount uses 1 indexing.
mTestIdInvocationCount.compute(description.toString(),
(key, value) -> (value == null) ? 1 : value + 1);
+
if (shouldRun(description)) {
try {
mTestData = createDataRecord();
@@ -345,6 +358,9 @@
}
mCollectIterationInterval = Integer.parseInt(args.getString(
COLLECT_ITERATION_INTERVAL, String.valueOf(DEFAULT_COLLECT_INTERVAL)));
+ mSkipMetricUntilIteration = Integer.parseInt(args.getString(
+ SKIP_METRIC_UNTIL_ITERATION, String.valueOf(SKIP_UNTIL_DEFAULT_ITERATION)));
+
if (mCollectIterationInterval < 1) {
Log.i(getTag(), "Metric collection iteration interval cannot be less than 1."
+ "Switching to collect for all the iterations.");
@@ -403,6 +419,7 @@
if (mLogOnly) {
return false;
}
+
MetricOption annotation = desc.getAnnotation(MetricOption.class);
List<String> groups = new ArrayList<>();
if (annotation != null) {
@@ -428,10 +445,21 @@
return false;
}
+ // Skip metric collection if current iteration is lesser than or equal to
+ // given skip until iteration count.
+ // mTestIdInvocationCount uses 1 indexing.
+ if (mTestIdInvocationCount.containsKey(desc.toString())
+ && mTestIdInvocationCount.get(desc.toString()) <= mSkipMetricUntilIteration) {
+ Log.i(getTag(), String.format("Skipping metric collection. Current iteration is %d."
+ + "Requested to skip metric until %d",
+ mTestIdInvocationCount.get(desc.toString()),
+ mSkipMetricUntilIteration));
+ return false;
+ }
+
// Check for iteration interval metric collection criteria.
- if ((mTestIdInvocationCount.containsKey(desc.toString()))
- && (mTestIdInvocationCount.get(desc.toString())
- % mCollectIterationInterval != 0)) {
+ if (mTestIdInvocationCount.containsKey(desc.toString())
+ && (mTestIdInvocationCount.get(desc.toString()) % mCollectIterationInterval != 0)) {
return false;
}
return true;
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
index 9ba0e8b..6f5ee06 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
@@ -23,6 +23,8 @@
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.helpers.PerfettoHelper;
+
+import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
@@ -31,6 +33,7 @@
import java.util.function.Supplier;
import org.junit.runner.Description;
import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
/**
* A {@link PerfettoListener} that captures the perfetto trace during each test method
@@ -61,6 +64,8 @@
// Collect per run if it is set to true otherwise collect per test.
public static final String COLLECT_PER_RUN = "per_run";
public static final String PERFETTO_PREFIX = "perfetto_";
+ // Skip failure metrics collection if this flag is set to true.
+ public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics";
private final WakeLockContext mWakeLockContext;
private final Supplier<WakeLock> mWakelockSupplier;
@@ -79,6 +84,8 @@
private boolean mPerfettoStartSuccess = false;
private boolean mIsConfigTextProto = false;
private boolean mIsCollectPerRun;
+ private boolean mSkipTestFailureMetrics;
+ private boolean mIsTestFailed = false;
private PerfettoHelper mPerfettoHelper = new PerfettoHelper();
@@ -150,6 +157,9 @@
// Defaulted to /sdcard/test_results if test_output_root is not passed.
mTestOutputRoot = args.getString(TEST_OUTPUT_ROOT, DEFAULT_OUTPUT_ROOT);
+ // By default this flag is set to false to collect the metrics on test failure.
+ mSkipTestFailureMetrics = "true".equals(args.getString(SKIP_TEST_FAILURE_METRICS));
+
if (!mIsCollectPerRun) {
return;
}
@@ -170,6 +180,7 @@
@Override
public void onTestStart(DataRecord testData, Description description) {
+ mIsTestFailed = false;
if (mIsCollectPerRun) {
return;
}
@@ -192,6 +203,11 @@
}
@Override
+ public void onTestFail(DataRecord testData, Description description, Failure failure) {
+ mIsTestFailed = true;
+ }
+
+ @Override
public void onTestEnd(DataRecord testData, Description description) {
if (mIsCollectPerRun) {
return;
@@ -205,30 +221,42 @@
return;
}
- Runnable task =
- () -> {
- Log.i(getTag(), "Stopping perfetto after test ended.");
- // Construct test output directory in the below format
- // <root_folder>/<test_display_name>/PerfettoListener/<test_display_name>-<count>.pb
- Path path =
- Paths.get(
- mTestOutputRoot,
- getTestFileName(description),
- this.getClass().getSimpleName(),
- String.format(
- "%s%s-%d.pb",
- PERFETTO_PREFIX,
- getTestFileName(description),
- mTestIdInvocationCount.get(
- getTestFileName(description))));
- stopPerfettoTracing(path, testData);
- };
-
- if (mHoldWakelockWhileCollecting) {
- Log.d(getTag(), "Holding a wakelock at onTestEnd.");
- mWakeLockContext.run(task);
+ Runnable task = null;
+ if (mSkipTestFailureMetrics && mIsTestFailed) {
+ Log.i(getTag(), "Skipping the metric collection due to test failure.");
+ // Stop the existing perfetto trace collection.
+ try {
+ if (!mPerfettoHelper.stopPerfetto()) {
+ Log.e(getTag(), "Failed to stop the perfetto process.");
+ }
+ } catch (IOException e) {
+ Log.e(getTag(), "Failed to stop the perfetto.", e);
+ }
} else {
- task.run();
+ task =
+ () -> {
+ Log.i(getTag(), "Stopping perfetto after test ended.");
+ // Construct test output directory in the below format
+ // <root_folder>/<test_name>/PerfettoListener/<test_name>-<count>.pb
+ Path path =
+ Paths.get(
+ mTestOutputRoot,
+ getTestFileName(description),
+ this.getClass().getSimpleName(),
+ String.format(
+ "%s%s-%d.pb",
+ PERFETTO_PREFIX,
+ getTestFileName(description),
+ mTestIdInvocationCount.get(
+ getTestFileName(description))));
+ stopPerfettoTracing(path, testData);
+ };
+ if (mHoldWakelockWhileCollecting) {
+ Log.d(getTag(), "Holding a wakelock at onTestEnd.");
+ mWakeLockContext.run(task);
+ } else {
+ task.run();
+ }
}
}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java
deleted file mode 100644
index 976fc3d..0000000
--- a/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2019 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.device.collectors;
-
-import android.device.collectors.annotations.OptionClass;
-import android.os.Bundle;
-import android.util.Log;
-import androidx.annotation.VisibleForTesting;
-import com.android.helpers.RssSnapshotHelper;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A {@link RssSnapshotListener} that takes a snapshot of Rss sizes for the list of
- * specified processes.
- *
- * Options:
- * -e process-names [processNames] : a comma-separated list of processes
- * -e drop-cache [pagecache | slab | all] : drop cache flag
- * -e test-output-dir [path] : path to the output directory
- */
-@OptionClass(alias = "rsssnapshot-collector")
-public class RssSnapshotListener extends BaseCollectionListener<String> {
- private static final String TAG = RssSnapshotListener.class.getSimpleName();
- private static final String DEFAULT_OUTPUT_DIR = "/sdcard/test_results";
-
- @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
- @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
- @VisibleForTesting static final String DROP_CACHE_KEY = "drop-cache";
- @VisibleForTesting static final String OUTPUT_DIR_KEY = "test-output-dir";
-
- private RssSnapshotHelper mRssSnapshotHelper = new RssSnapshotHelper();
- private final Map<String, Integer> dropCacheValues = new HashMap<String, Integer>() {
- {
- put("pagecache", 1);
- put("slab", 2);
- put("all", 3);
- }
- };
-
- public RssSnapshotListener() {
- createHelperInstance(mRssSnapshotHelper);
- }
-
- /**
- * Constructor to simulate receiving the instrumentation arguments. Should not be used except
- * for testing.
- */
- @VisibleForTesting
- public RssSnapshotListener(Bundle args, RssSnapshotHelper helper) {
- super(args, helper);
- mRssSnapshotHelper = helper;
- createHelperInstance(mRssSnapshotHelper);
- }
-
- /**
- * Adds the options for rss snapshot collector.
- */
- @Override
- public void setupAdditionalArgs() {
- Bundle args = getArgsBundle();
- String testOutputDir = args.getString(OUTPUT_DIR_KEY, DEFAULT_OUTPUT_DIR);
- String procsString = args.getString(PROCESS_NAMES_KEY);
- if (procsString == null) {
- Log.e(TAG, "No processes provided to sample");
- return;
- }
- String[] procs = procsString.split(PROCESS_SEPARATOR);
-
- mRssSnapshotHelper.setUp(testOutputDir, procs);
-
- String dropCacheValue = args.getString(DROP_CACHE_KEY);
- if (dropCacheValue != null) {
- if (dropCacheValues.containsKey(dropCacheValue)) {
- mRssSnapshotHelper.setDropCacheOption(dropCacheValues.get(dropCacheValue));
- } else {
- Log.e(TAG, "Value for \"" + DROP_CACHE_KEY + "\" parameter is invalid");
- return;
- }
- }
- }
-}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java
new file mode 100644
index 0000000..ce319c3
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.helpers.ICollectorHelper;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+
+/**
+ * Extend this class for a periodic metric collection which relies on ICollectorHelper to collect
+ * metrics and dump the time-series in csv format. In case of system crashes, the time series up to
+ * the point where the crash happened will still be stored.
+ *
+ * In case of running tests with Tradefed file pulller, use the option
+ * {@link file-puller-log-collector:directory-keys} from {{@link FilePullerLogCollector} to
+ * specify the directory path under which the output file should be pulled from (i.e.
+ * <external_storage>/test_results, where <external_storage> is /sdcard for Android phones and
+ * /storage/emulated/10 for Android Auto), instead of using
+ * {@link file-puller-log-collector:pull-pattern-keys}.
+ */
+public class ScheduledRunCollectionListener<T extends Number> extends ScheduledRunMetricListener {
+ private static final String LOG_TAG = ScheduledRunCollectionListener.class.getSimpleName();
+ private static final String TIME_SERIES_PREFIX = "time_series_";
+ @VisibleForTesting public static final String OUTPUT_ROOT = "test_results";
+ @VisibleForTesting public static final String OUTPUT_FILE_PATH = "%s_time_series_path";
+
+ @VisibleForTesting
+ public static final String TIME_SERIES_HEADER =
+ String.format("%-20s,%-100s,%-20s", "time", "metric_key", "value");
+
+ private static final String TIME_SERIES_BODY = "%-20d,%-100s,%-20s";
+ @VisibleForTesting public static final String MEAN_SUFFIX = "-mean";
+ @VisibleForTesting public static final String MAX_SUFFIX = "-max";
+ @VisibleForTesting public static final String MIN_SUFFIX = "-min";
+
+ protected ICollectorHelper<T> mHelper;
+ private TimeSeriesCsvWriter mTimeSeriesCsvWriter;
+ private TimeSeriesStatistics mTimeSeriesStatistics;
+ private long mStartTime;
+
+ public ScheduledRunCollectionListener() {}
+
+ @VisibleForTesting
+ ScheduledRunCollectionListener(Bundle argsBundle, ICollectorHelper helper) {
+ super(argsBundle);
+ mHelper = helper;
+ }
+
+ /**
+ * Write a time-series in csv format to the given destination under external storage as an
+ * unpivoted table like:
+ *
+ * time ,metric_key ,value
+ * 0 ,metric1 ,5
+ * 0 ,metric2 ,10
+ * 0 ,metric3 ,15
+ * 1000 ,metric1 ,6
+ * 1000 ,metric2 ,11
+ * 1000 ,metric3 ,16
+ */
+ private class TimeSeriesCsvWriter {
+ private File mDestFile;
+ private boolean mIsHeaderWritten = false;
+
+ private TimeSeriesCsvWriter(Path destination) {
+ // Create parent directory if it doesn't exist.
+ File destDir = createAndEmptyDirectory(destination.getParent().toString());
+ mDestFile = new File(destDir, destination.getFileName().toString());
+ }
+
+ private void write(Map<String, T> dataPoint, long timeStamp) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(mDestFile, true))) {
+ if (!mIsHeaderWritten) {
+ writer.append(TIME_SERIES_HEADER);
+ writer.append("\n");
+ mIsHeaderWritten = true;
+ }
+
+ for (String key : dataPoint.keySet()) {
+ writer.append(
+ String.format(TIME_SERIES_BODY, timeStamp, key, dataPoint.get(key)));
+ writer.append("\n");
+ }
+ } catch (IOException e) {
+ Log.e(
+ LOG_TAG,
+ String.format("Fail to output time series due to : %s.", e.getMessage()));
+ }
+ }
+ }
+
+ private class TimeSeriesStatistics {
+ Map<String, T> minMap = new HashMap<>();
+ Map<String, T> maxMap = new HashMap<>();
+ Map<String, Double> sumMap = new HashMap<>();
+ Map<String, Long> countMap = new HashMap<>();
+
+ private void update(Map<String, T> dataPoint) {
+ for (String key : dataPoint.keySet()) {
+ T value = dataPoint.get(key);
+ // Add / replace min.
+ minMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == -1 ? value : v);
+ minMap.computeIfAbsent(key, k -> value);
+ // Add / replace max.
+ maxMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == 1 ? value : v);
+ maxMap.computeIfAbsent(key, k -> value);
+ // Add / update sum.
+ sumMap.put(key, value.doubleValue() + sumMap.getOrDefault(key, 0.));
+ // Add / update count.
+ countMap.put(key, 1 + countMap.getOrDefault(key, 0L));
+ }
+ }
+
+ private Map<String, String> getStatistics() {
+ Map<String, String> res = new HashMap<>();
+ for (String key : minMap.keySet()) {
+ res.put(key + MIN_SUFFIX, minMap.get(key).toString());
+ }
+ for (String key : maxMap.keySet()) {
+ res.put(key + MAX_SUFFIX, maxMap.get(key).toString());
+ }
+ for (String key : sumMap.keySet()) {
+ if (countMap.containsKey(key)) {
+ double mean = sumMap.get(key) / countMap.get(key);
+ res.put(key + MEAN_SUFFIX, Double.toString(mean));
+ }
+ }
+ return res;
+ }
+
+ /** Compare to Number objects. Return -1 if the n1 < n2; 0 if n1 == n2; 1 if n1 > n2. */
+ private int compareAsDouble(Number n1, Number n2) {
+ Double d1 = Double.valueOf(n1.doubleValue());
+ Double d2 = Double.valueOf(n2.doubleValue());
+ return d1.compareTo(d2);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ void onStart(DataRecord runData, Description description) {
+ setupAdditionalArgs();
+ Path path =
+ Paths.get(
+ OUTPUT_ROOT,
+ getClass().getSimpleName(),
+ String.format(
+ "%s%s-%d.csv",
+ TIME_SERIES_PREFIX,
+ getClass().getSimpleName(),
+ UUID.randomUUID().hashCode()));
+ mTimeSeriesCsvWriter = new TimeSeriesCsvWriter(path);
+ mTimeSeriesStatistics = new TimeSeriesStatistics();
+ mStartTime = SystemClock.uptimeMillis();
+ mHelper.startCollecting();
+ // Send to stdout the path where the time-series files will be stored.
+ Bundle filePathBundle = new Bundle();
+ filePathBundle.putString(
+ String.format(OUTPUT_FILE_PATH, getClass().getSimpleName()),
+ mTimeSeriesCsvWriter.mDestFile.toString());
+ SendToInstrumentation.sendBundle(getInstrumentation(), filePathBundle);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ void onEnd(DataRecord runData, Result result) {
+ mHelper.stopCollecting();
+ for (Map.Entry<String, String> entry : mTimeSeriesStatistics.getStatistics().entrySet()) {
+ runData.addStringMetric(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void collect(DataRecord runData, Description description) throws InterruptedException {
+ long timeStamp = SystemClock.uptimeMillis() - mStartTime;
+ Map<String, T> dataPoint = mHelper.getMetrics();
+ mTimeSeriesCsvWriter.write(dataPoint, timeStamp);
+ mTimeSeriesStatistics.update(dataPoint);
+ }
+
+ /**
+ * To add listener specific extra args implement this method in the sub class and add the
+ * listener specific args.
+ */
+ public void setupAdditionalArgs() {
+ // NO-OP by default
+ }
+
+ protected void createHelperInstance(ICollectorHelper helper) {
+ mHelper = helper;
+ }
+}
+
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
new file mode 100644
index 0000000..69f7085
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.HashMap;
+
+import org.junit.runner.Description;
+
+/**
+ * A {@link BaseMetricListener} that captures video of the screen.
+ *
+ * <p>This class needs external storage permission. See {@link BaseMetricListener} how to grant
+ * external storage permission, especially at install time.
+ */
+@OptionClass(alias = "screen-record-collector")
+public class ScreenRecordCollector extends BaseMetricListener {
+ @VisibleForTesting static final int MAX_RECORDING_PARTS = 5;
+ private static final long VIDEO_TAIL_BUFFER = 2000;
+
+ static final String OUTPUT_DIR = "run_listeners/videos";
+
+ private UiDevice mDevice;
+ private File mDestDir;
+
+ // Tracks multiple parts to a single recording.
+ private int mParts;
+ // Avoid recording after the test is finished.
+ private boolean mContinue;
+
+ // Tracks the test iterations to ensure that each failure gets unique filenames.
+ // Key: test description; value: number of iterations.
+ private Map<String, Integer> mTestIterations = new HashMap<String, Integer>();
+
+ @Override
+ public void onTestRunStart(DataRecord runData, Description description) {
+ mDestDir = createAndEmptyDirectory(OUTPUT_DIR);
+ }
+
+ @Override
+ public void onTestStart(DataRecord testData, Description description) {
+ if (mDestDir == null) {
+ return;
+ }
+
+ // Track the number of iteration for this test.
+ amendIterations(description);
+ // Start the screen recording operation.
+ mParts = 1;
+ mContinue = true;
+ startScreenRecordThread(description);
+ }
+
+ @Override
+ public void onTestEnd(DataRecord testData, Description description) {
+ // Skip if not directory.
+ if (mDestDir == null) {
+ return;
+ }
+
+ // Add some extra time to the video end.
+ SystemClock.sleep(getTailBuffer());
+ // Ctrl + C all screen record processes.
+ mContinue = false;
+ killScreenRecordProcesses();
+
+ // Add the output files to the data record.
+ for (int i = 1; i < mParts; i++) {
+ File output = getOutputFile(description, i);
+ testData.addFileMetric(String.format("%s_%s", getTag(), output.getName()), output);
+ }
+
+ // TODO(b/144869954): Delete when tests pass.
+ }
+
+ /** Updates the number of iterations performed for a given test {@link Description}. */
+ private void amendIterations(Description description) {
+ String testName = description.getDisplayName();
+ mTestIterations.computeIfPresent(testName, (name, iterations) -> iterations + 1);
+ mTestIterations.computeIfAbsent(testName, name -> 1);
+ }
+
+ private File getOutputFile(Description description, int part) {
+ final String baseName =
+ String.format("%s.%s", description.getClassName(), description.getMethodName());
+ // Omit the iteration number for the first iteration.
+ int iteration = mTestIterations.get(description.getDisplayName());
+ final String fileName =
+ String.format(
+ "%s-video%s.mp4",
+ iteration == 1
+ ? baseName
+ : String.join("-", baseName, String.valueOf(iteration)),
+ part == 1 ? "" : part);
+ return Paths.get(mDestDir.getAbsolutePath(), fileName).toFile();
+ }
+
+ /** Spawns a thread to start screen recording that will save to the provided {@code path}. */
+ public void startScreenRecordThread(final Description description) {
+ new Thread("test-screenrecord-thread") {
+ @Override
+ public void run() {
+ try {
+ for (int i = 0; i < MAX_RECORDING_PARTS && mContinue; i++) {
+ String output = getOutputFile(description, mParts).getAbsolutePath();
+ Log.d(getTag(), String.format("Recording screen to %s", output));
+ // Make sure not to block on this background command so the test runs.
+ getDevice().executeShellCommand(String.format("screenrecord %s", output));
+ mParts++;
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Caught exception while screen recording.");
+ }
+ }
+ }.start();
+ }
+
+ /** Kills all screen recording processes that are actively running on the device. */
+ public void killScreenRecordProcesses() {
+ try {
+ // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
+ String[] pids = getDevice().executeShellCommand("pidof screenrecord").split(" ");
+ for (String pid : pids) {
+ // Avoid empty process ids, because of weird splitting behavior.
+ if (pid.isEmpty()) {
+ continue;
+ }
+
+ getDevice().executeShellCommand(String.format("kill -2 %s", pid));
+ Log.d(getTag(), String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to kill screen recording process.");
+ }
+ }
+
+ /** Returns a buffer duration for the end of the video. */
+ @VisibleForTesting
+ public long getTailBuffer() {
+ return VIDEO_TAIL_BUFFER;
+ }
+
+ /** Returns the currently active {@link UiDevice}. */
+ public UiDevice getDevice() {
+ if (mDevice == null) {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ }
+ return mDevice;
+ }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java
new file mode 100644
index 0000000..e2fb8ab
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java
@@ -0,0 +1,120 @@
+/*
+ * 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import com.android.helpers.ShowmapSnapshotHelper;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link ShowmapSnapshotListener} that takes a snapshot of memory sizes for the list of
+ * specified processes.
+ *
+ * Options:
+ * -e process-names [processNames] : a comma-separated list of processes
+ * -e drop-cache [pagecache | slab | all] : drop cache flag
+ * -e test-output-dir [path] : path to the output directory
+ * -e metric-index [rss:2,pss:3,privatedirty:7] : memory metric name corresponding
+ * to index in the showmap output.
+ */
+@OptionClass(alias = "showmapsnapshot-collector")
+public class ShowmapSnapshotListener extends BaseCollectionListener<String> {
+ private static final String TAG = ShowmapSnapshotListener.class.getSimpleName();
+ private static final String DEFAULT_OUTPUT_DIR = "/sdcard/test_results";
+
+ @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
+ @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
+ @VisibleForTesting static final String METRIC_NAME_INDEX = "metric-name-index";
+ @VisibleForTesting static final String DROP_CACHE_KEY = "drop-cache";
+ @VisibleForTesting static final String OUTPUT_DIR_KEY = "test-output-dir";
+
+ private ShowmapSnapshotHelper mShowmapSnapshotHelper = new ShowmapSnapshotHelper();
+ private final Map<String, Integer> dropCacheValues = new HashMap<String, Integer>() {
+ {
+ put("pagecache", 1);
+ put("slab", 2);
+ put("all", 3);
+ }
+ };
+
+ // Sample output
+ // -------- -------- -------- -------- -------- -------- -------- -------- -------- ------ --
+ // virtual shared shared private private
+ // size RSS PSS clean dirty clean dirty swap swapPSS flags object
+ // ------- -------- -------- -------- -------- -------- -------- -------- -------- ----- ---
+ // 10810272 5400 1585 3800 168 264 1168 0 0 TOTAL
+
+ // Default to collect rss, pss and private dirty.
+ private String mMemoryMetricNameIndex = "rss:1,pss:2,privatedirty:6";
+
+ public ShowmapSnapshotListener() {
+ createHelperInstance(mShowmapSnapshotHelper);
+ }
+
+ /**
+ * Constructor to simulate receiving the instrumentation arguments. Should not be used except
+ * for testing.
+ */
+ @VisibleForTesting
+ public ShowmapSnapshotListener(Bundle args, ShowmapSnapshotHelper helper) {
+ super(args, helper);
+ mShowmapSnapshotHelper = helper;
+ createHelperInstance(mShowmapSnapshotHelper);
+ }
+
+ /**
+ * Adds the options for showmap snapshot collector.
+ */
+ @Override
+ public void setupAdditionalArgs() {
+ Bundle args = getArgsBundle();
+ String testOutputDir = args.getString(OUTPUT_DIR_KEY, DEFAULT_OUTPUT_DIR);
+ // Collect for all processes if process list is empty or null.
+ String procsString = args.getString(PROCESS_NAMES_KEY);
+
+ // Metric name and corresponding index in the output of showmap summary.
+ String metricNameIndexArg = args.getString(METRIC_NAME_INDEX);
+ if (metricNameIndexArg != null && !metricNameIndexArg.isEmpty()) {
+ mMemoryMetricNameIndex = metricNameIndexArg;
+ }
+ mShowmapSnapshotHelper.setMetricNameIndex(mMemoryMetricNameIndex);
+
+ String[] procs = null;
+ if (procsString == null || procsString.isEmpty()) {
+ mShowmapSnapshotHelper.setAllProcesses();
+ } else {
+ procs = procsString.split(PROCESS_SEPARATOR);
+ }
+
+
+ mShowmapSnapshotHelper.setUp(testOutputDir, procs);
+
+ String dropCacheValue = args.getString(DROP_CACHE_KEY);
+ if (dropCacheValue != null) {
+ if (dropCacheValues.containsKey(dropCacheValue)) {
+ mShowmapSnapshotHelper.setDropCacheOption(dropCacheValues.get(dropCacheValue));
+ } else {
+ Log.e(TAG, "Value for \"" + DROP_CACHE_KEY + "\" parameter is invalid");
+ return;
+ }
+ }
+ }
+}
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
index acd59ed..a6bc18c 100644
--- a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
@@ -9,11 +9,14 @@
## Checking in a config
-To check in a config, follow these steps:
+To check in config(s) for a new set of metrics, follow these steps:
-1. Create a directory under this directory for the new config (e.g. `app-start`).
-2. Put the new config in the subdirectory using the directory name + `.pb` extension.
-3. Write a README file explaining what the config does and put it under the new subdirectory.
+1. Create a directory under this directory for the new metrics (e.g. `app-start`).
+2. Put the new config(s) in the subdirectory using the directory name and optionally with additional
+suffixes if there are multiple configs related to the overarching metrics, with `.pb` extension.
+This ensures that each config has a unique name.
+3. Write a README file explaining what the config(s) in the new subdirectory does and put it under
+the new subdirectory.
# (Internal only) Creating a config
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md
new file mode 100644
index 0000000..e8985eb
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md
@@ -0,0 +1,3 @@
+# Greenday Power Configs for Run Level and Test Level Metrics
+
+Configs for the following power metrics : CPUClusterTime, CPUTimePerFreq, CPUTimePerThreadFreq, CPUTimePerUidFreq, MobileBytesTransfer, ProcessCPUTime, RemainingBatteryCapacity, SubsystemSleepState and WifiBytesTransfer.
\ No newline at end of file
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb
new file mode 100644
index 0000000..e2bcee6
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb
new file mode 100644
index 0000000..54078a0
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md
new file mode 100644
index 0000000..558107a
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md
@@ -0,0 +1,4 @@
+# Remaining Battery Capacity Configs
+
+These configs are used to collect the remaining battery capacity on the device as defined in the
+RemainingBatteryCapacity (Colomb counter) atom.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb
new file mode 100644
index 0000000..ab1f5c2
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb
new file mode 100644
index 0000000..55a8f3d
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md
new file mode 100644
index 0000000..63c917e
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md
@@ -0,0 +1,4 @@
+# WiFi Bytes Transfer Configs
+
+The configs here collects the WiFi bytes transferred on the start and end of tests or test run,
+respectively.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb
new file mode 100644
index 0000000..ef522ac
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb
new file mode 100644
index 0000000..f022f32
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
index cd05278..97f4719 100644
--- a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
@@ -21,7 +21,9 @@
import android.content.res.AssetManager;
import android.os.Bundle;
import android.os.Environment;
+import android.os.SystemClock;
import android.util.Log;
+import android.util.StatsLog;
import androidx.annotation.VisibleForTesting;
import androidx.test.InstrumentationRegistry;
@@ -43,6 +45,7 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -72,6 +75,12 @@
// Common prefix for the metric file.
static final String REPORT_FILENAME_PREFIX = "statsd-";
+ // Labels used to signify test events to statsd with the AppBreadcrumbReported atom.
+ static final int RUN_EVENT_LABEL = 7;
+ static final int TEST_EVENT_LABEL = 11;
+ // A short delay after pushing the AppBreadcrumbReported event so that metrics can be dumped.
+ static final long METRIC_PULL_DELAY = TimeUnit.SECONDS.toMillis(1);
+
// Configs used for the test run and each test, respectively.
private Map<String, StatsdConfig> mRunLevelConfigs = new HashMap<String, StatsdConfig>();
private Map<String, StatsdConfig> mTestLevelConfigs = new HashMap<String, StatsdConfig>();
@@ -95,6 +104,10 @@
mTestLevelConfigs.putAll(getConfigsFromOption(OPTION_CONFIGS_TEST_LEVEL));
mRunLevelConfigIds = registerConfigsWithStatsManager(mRunLevelConfigs);
+
+ if (!logStart(RUN_EVENT_LABEL)) {
+ Log.w(LOG_TAG, "Failed to log a test run start event. Metrics might be incomplete.");
+ }
}
/**
@@ -104,6 +117,11 @@
*/
@Override
public void onTestRunEnd(DataRecord runData, Result result) {
+ if (!logStop(RUN_EVENT_LABEL)) {
+ Log.w(LOG_TAG, "Failed to log a test run end event. Metrics might be incomplete.");
+ }
+ SystemClock.sleep(METRIC_PULL_DELAY);
+
Map<String, File> configReports =
pullReportsAndRemoveConfigs(
mRunLevelConfigIds, Paths.get(REPORT_PATH_ROOT, REPORT_PATH_RUN_LEVEL), "");
@@ -118,6 +136,10 @@
mTestIterations.computeIfPresent(description.getDisplayName(), (name, count) -> count + 1);
mTestIterations.computeIfAbsent(description.getDisplayName(), name -> 1);
mTestLevelConfigIds = registerConfigsWithStatsManager(mTestLevelConfigs);
+
+ if (!logStart(TEST_EVENT_LABEL)) {
+ Log.w(LOG_TAG, "Failed to log a test start event. Metrics might be incomplete.");
+ }
}
/**
@@ -127,6 +149,11 @@
*/
@Override
public void onTestEnd(DataRecord testData, Description description) {
+ if (!logStop(TEST_EVENT_LABEL)) {
+ Log.w(LOG_TAG, "Failed to log a test end event. Metrics might be incomplete.");
+ }
+ SystemClock.sleep(METRIC_PULL_DELAY);
+
Map<String, File> configReports =
pullReportsAndRemoveConfigs(
mTestLevelConfigIds,
@@ -411,4 +438,24 @@
configName ->
parseConfigFromName(manager, optionName, configName)));
}
+
+ /**
+ * Log a "start" AppBreadcrumbReported event to statsd. Wraps a static method for testing.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected boolean logStart(int label) {
+ return StatsLog.logStart(label);
+ }
+
+ /**
+ * Log a "stop" AppBreadcrumbReported event to statsd. Wraps a static method for testing.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected boolean logStop(int label) {
+ return StatsLog.logStop(label);
+ }
}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
index 79f16d0..af6d75b 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
@@ -150,7 +150,7 @@
mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
verify(helper, times(0)).startCollecting();
- mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+ mListener.testStarted(FAKE_TEST_DESCRIPTION);
verify(helper, times(1)).startCollecting();
Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
new Exception());
@@ -171,7 +171,7 @@
mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
verify(helper, times(0)).startCollecting();
- mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+ mListener.testStarted(FAKE_TEST_DESCRIPTION);
verify(helper, times(1)).startCollecting();
Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
new Exception());
@@ -195,7 +195,7 @@
mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
verify(helper, times(0)).startCollecting();
- mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+ mListener.testStarted(FAKE_TEST_DESCRIPTION);
verify(helper, times(1)).startCollecting();
Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
new Exception());
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
index 11cba99..0931070 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
@@ -478,6 +478,66 @@
}
/**
+ * Metric collection does not happen on the skipped iterations.
+ */
+ @MetricOption(group = "testGroup")
+ @Test
+ public void testSameMethodNameWithSkipIterationOption() throws Exception {
+ Bundle args = new Bundle();
+ args.putString(BaseMetricListener.SKIP_METRIC_UNTIL_ITERATION, "2");
+ mListener = createWithArgs(args);
+ mListener.setInstrumentation(mMockInstrumentation);
+
+ // Skip until iteration is set to 2.
+ // Metric will not be collected for 1st and 2nd iterations.
+ Description runDescription = Description.createSuiteDescription("run");
+ mListener.testRunStarted(runDescription);
+ Description testDescription = Description.createTestDescription("class", "method");
+ mListener.testStarted(testDescription);
+ mListener.testFinished(testDescription);
+ mListener.testStarted(testDescription);
+ mListener.testFinished(testDescription);
+ mListener.testStarted(testDescription);
+ mListener.testFinished(testDescription);
+ mListener.testStarted(testDescription);
+ mListener.testFinished(testDescription);
+ mListener.testStarted(testDescription);
+ mListener.testFinished(testDescription);
+ mListener.testRunFinished(new Result());
+ // AJUR runner is then gonna call instrumentationRunFinished
+ Bundle resultBundle = new Bundle();
+ mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+ // Check instrumentation status inprogress called only 3 times.
+ ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+ Mockito.verify(mMockInstrumentation, Mockito.times(3))
+ .sendStatus(Mockito.eq(
+ SendToInstrumentation.INST_STATUS_IN_PROGRESS), capture.capture());
+
+ List<Bundle> capturedBundle = capture.getAllValues();
+ assertEquals(3, capturedBundle.size());
+ Bundle check = capturedBundle.get(0);
+ assertEquals(TEST_START_VALUE + "method", check.getString(TEST_START_KEY));
+ assertEquals(TEST_END_VALUE + "method", check.getString(TEST_END_KEY));
+ assertEquals(2, check.size());
+
+ Bundle check2 = capturedBundle.get(1);
+ assertEquals(TEST_START_VALUE + "method", check2.getString(TEST_START_KEY));
+ assertEquals(TEST_END_VALUE + "method", check2.getString(TEST_END_KEY));
+ assertEquals(2, check2.size());
+
+ Bundle check3 = capturedBundle.get(2);
+ assertEquals(TEST_START_VALUE + "method", check3.getString(TEST_START_KEY));
+ assertEquals(TEST_END_VALUE + "method", check3.getString(TEST_END_KEY));
+ assertEquals(2, check2.size());
+
+ // Check that final bundle contains run results
+ assertEquals(RUN_START_VALUE, resultBundle.getString(RUN_START_KEY));
+ assertEquals(RUN_END_VALUE, resultBundle.getString(RUN_END_KEY));
+ assertEquals(2, resultBundle.size());
+ }
+
+ /**
* Metric collection happens on all the iteration if the interval is
* invalid (i.e less than 1).
*/
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
index 20ae4d6..859bf01 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
@@ -40,12 +40,14 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
+
/**
* Android Unit tests for {@link PerfettoListener}.
*
@@ -58,6 +60,9 @@
// A {@code Description} to pass when faking a test run start call.
private static final Description FAKE_DESCRIPTION = Description.createSuiteDescription("run");
+ private static final Description FAKE_TEST_DESCRIPTION = Description
+ .createTestDescription("class", "method");
+
private Description mRunDesc;
private Description mTest1Desc;
private Description mTest2Desc;
@@ -128,6 +133,62 @@
}
/*
+ * Verify stop collecting called exactly once when the test failed and the
+ * skip test failure mmetrics is enabled.
+ */
+ @Test
+ public void testPerfettoPerTestFailureFlowDefault() throws Exception {
+ Bundle b = new Bundle();
+ b.putString(PerfettoListener.SKIP_TEST_FAILURE_METRICS, "false");
+ mListener = initListener(b);
+
+ doReturn(true).when(mPerfettoHelper).startCollecting(anyString(), anyBoolean());
+ doReturn(true).when(mPerfettoHelper).stopCollecting(anyLong(), anyString());
+ // Test run start behavior
+ mListener.testRunStarted(mRunDesc);
+
+ // Test test start behavior
+ mListener.testStarted(mTest1Desc);
+ verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
+
+ // Test fail behaviour
+ Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
+ new Exception());
+ mListener.onTestFail(mDataRecord, mTest1Desc, failureDesc);
+ mListener.onTestEnd(mDataRecord, mTest1Desc);
+ verify(mPerfettoHelper, times(1)).stopCollecting(anyLong(), anyString());
+
+ }
+
+ /*
+ * Verify stop perfetto called exactly once when the test failed and the
+ * skip test failure metrics is enabled.
+ */
+ @Test
+ public void testPerfettoPerTestFailureFlowWithSkipMmetrics() throws Exception {
+ Bundle b = new Bundle();
+ b.putString(PerfettoListener.SKIP_TEST_FAILURE_METRICS, "true");
+ mListener = initListener(b);
+
+ doReturn(true).when(mPerfettoHelper).startCollecting(anyString(), anyBoolean());
+ doReturn(true).when(mPerfettoHelper).stopPerfetto();
+ // Test run start behavior
+ mListener.testRunStarted(mRunDesc);
+
+ // Test test start behavior
+ mListener.testStarted(mTest1Desc);
+ verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
+
+ // Test fail behaviour
+ Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
+ new Exception());
+ mListener.onTestFail(mDataRecord, mTest1Desc, failureDesc);
+ mListener.onTestEnd(mDataRecord, mTest1Desc);
+ verify(mPerfettoHelper, times(1)).stopPerfetto();
+
+ }
+
+ /*
* Verify perfetto start and stop collection methods called exactly once for test run.
* and not during each test method.
*/
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java
new file mode 100644
index 0000000..f573dc2
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.ICollectorHelper;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Android Unit tests for {@link ScheduledRunCollectionListener}. */
+@RunWith(AndroidJUnit4.class)
+public class ScheduledRunCollectionListenerTest {
+
+ private static final String TEST_METRIC_KEY = "test_metric_key";
+ private static final Integer[] TEST_METRIC_VALUES = {0, 1, 2, 3, 4};
+ private static final long TEST_INTERVAL = 100L;
+ private static final long TEST_DURATION = 450L;
+ // Collects at 0ms, 100ms, 200ms, and so on.
+ private static final long NUMBER_OF_COLLECTIONS = TEST_DURATION / TEST_INTERVAL + 1;
+ private static final String DATA_REGEX =
+ "(?<timestamp>[0-9]+)\\s+," + TEST_METRIC_KEY + "\\s+,(?<value>[0-9])\\s+";
+
+ @Mock private ICollectorHelper mHelper;
+
+ @Mock private Instrumentation mInstrumentation;
+
+ private ScheduledRunCollectionListener mListener;
+
+ private ScheduledRunCollectionListener initListener() {
+ Bundle b = new Bundle();
+ b.putString(ScheduledRunCollectionListener.INTERVAL_ARG_KEY, Long.toString(TEST_INTERVAL));
+ doReturn(true).when(mHelper).startCollecting();
+ Map<String, Integer> first = new HashMap<>();
+ first.put(TEST_METRIC_KEY, TEST_METRIC_VALUES[0]);
+ Map<String, Integer>[] rest =
+ Arrays.stream(TEST_METRIC_VALUES)
+ .skip(1)
+ .map(
+ testMetricValue -> {
+ Map<String, Integer> m = new HashMap<>();
+ m.put(TEST_METRIC_KEY, testMetricValue);
+ return m;
+ })
+ .toArray(Map[]::new);
+ // <code>thenReturn</code> call signature requires thenReturn(T value, T... values).
+ when(mHelper.getMetrics()).thenReturn(first, rest);
+ doReturn(true).when(mHelper).stopCollecting();
+ ScheduledRunCollectionListener listener =
+ new ScheduledRunCollectionListener<Integer>(b, mHelper);
+ // Mock getUiAutomation method for the purpose of enabling createAndEmptyDirectory method
+ // from BaseMetricListener.
+ doReturn(InstrumentationRegistry.getInstrumentation().getUiAutomation())
+ .when(mInstrumentation)
+ .getUiAutomation();
+ listener.setInstrumentation(mInstrumentation);
+ return listener;
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mListener = initListener();
+ }
+
+ @After
+ public void tearDown() {
+ // Remove files/directories that have been created.
+ Path outputFilePath =
+ Paths.get(
+ Environment.getExternalStorageDirectory().toString(),
+ ScheduledRunCollectionListener.OUTPUT_ROOT,
+ ScheduledRunCollectionListener.class.getSimpleName());
+ mListener.executeCommandBlocking("rm -rf " + outputFilePath.toString());
+ }
+
+ @Test
+ public void testCompleteRun() throws Exception {
+ testRun(true);
+ }
+
+ @Test
+ public void testIncompleteRun() throws Exception {
+ testRun(false);
+ }
+
+ @Test
+ public void testInstrumentationResult() throws Exception {
+ Description runDescription = Description.createSuiteDescription("run");
+ mListener.testRunStarted(runDescription);
+
+ Thread.sleep(TEST_DURATION);
+ mListener.testRunFinished(new Result());
+ // AJUR runner is then gonna call instrumentationRunFinished.
+ Bundle result = new Bundle();
+ mListener.instrumentationRunFinished(System.out, result, new Result());
+ int expectedMin = Arrays.stream(TEST_METRIC_VALUES).min(Integer::compare).get();
+ assertEquals(
+ expectedMin,
+ Integer.parseInt(
+ result.getString(
+ TEST_METRIC_KEY + ScheduledRunCollectionListener.MIN_SUFFIX)));
+ int expectedMax = Arrays.stream(TEST_METRIC_VALUES).max(Integer::compare).get();
+ assertEquals(
+ expectedMax,
+ Integer.parseInt(
+ result.getString(
+ TEST_METRIC_KEY + ScheduledRunCollectionListener.MAX_SUFFIX)));
+ double expectedMean =
+ Arrays.stream(TEST_METRIC_VALUES).mapToDouble(i -> i.doubleValue()).sum()
+ / NUMBER_OF_COLLECTIONS;
+ assertEquals(
+ expectedMean,
+ Double.parseDouble(
+ result.getString(
+ TEST_METRIC_KEY + ScheduledRunCollectionListener.MEAN_SUFFIX)),
+ 0.1);
+ }
+
+ private void testRun(boolean isComplete) throws Exception {
+ Description runDescription = Description.createSuiteDescription("run");
+ mListener.testRunStarted(runDescription);
+
+ Thread.sleep(TEST_DURATION);
+ // If incomplete run happens, for example, when a system crash happens half way through the
+ // run, <code>testRunFinished</code> method will be skipped, but the output file should
+ // still be present, and the time-series up to the point when the crash happens should still
+ // be recorded.
+ if (isComplete) {
+ mListener.testRunFinished(new Result());
+ }
+
+ ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
+
+ // Verify that the path of the time-series output file has been sent to instrumentation.
+ verify(mInstrumentation, atLeast(1))
+ .sendStatus(eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS), bundle.capture());
+ Bundle pathBundle = bundle.getAllValues().get(0);
+ String pathKey =
+ String.format(
+ ScheduledRunCollectionListener.OUTPUT_FILE_PATH,
+ ScheduledRunCollectionListener.class.getSimpleName());
+ String path = pathBundle.getString(pathKey);
+ assertNotNull(path);
+
+ // Check the output file exists.
+ File outputFile = new File(path);
+ assertTrue(outputFile.exists());
+
+ // Check that output file contains some of the periodic run results, sample results are
+ // like:
+ //
+ // time ,metric_key ,value
+ // 2 ,test_metric_key ,0
+ // 102 ,test_metric_key ,0
+ // 203 ,test_metric_key ,0
+ // ...
+ List<String> lines = Files.readAllLines(outputFile.toPath(), Charset.defaultCharset());
+ assertEquals(NUMBER_OF_COLLECTIONS, lines.size() - 1);
+ assertEquals(lines.get(0), ScheduledRunCollectionListener.TIME_SERIES_HEADER);
+ for (int i = 1; i != lines.size(); ++i) {
+ Pattern p = Pattern.compile(DATA_REGEX);
+ Matcher m = p.matcher(lines.get(i));
+ assertTrue(m.matches());
+ long timestamp = Long.parseLong(m.group("timestamp"));
+ long delta = TEST_INTERVAL / 2;
+ assertEquals((i - 1) * TEST_INTERVAL, timestamp, delta);
+ Integer value = Integer.valueOf(m.group("value"));
+ assertEquals(TEST_METRIC_VALUES[i - 1], value);
+ }
+
+ // For incomplete run, invoke testRunFinished in the end to prevent resource leak.
+ if (!isComplete) {
+ mListener.testRunFinished(new Result());
+ }
+ }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
new file mode 100644
index 0000000..8f45a74
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.endsWith;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Android Unit tests for {@link ScreenRecordCollector}.
+ *
+ * <p>To run: atest CollectorDeviceLibTest:android.device.collectors.ScreenRecordCollectorTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ScreenRecordCollectorTest {
+
+ private static final int NUM_TEST_CASE = 10;
+
+ private File mLogDir;
+ private Description mRunDesc;
+ private Description mTestDesc;
+ private ScreenRecordCollector mListener;
+
+ @Mock private Instrumentation mInstrumentation;
+
+ @Mock private UiDevice mDevice;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLogDir = new File("tmp/");
+ mRunDesc = Description.createSuiteDescription("run");
+ mTestDesc = Description.createTestDescription("run", "test");
+ }
+
+ @After
+ public void tearDown() {
+ if (mLogDir != null) {
+ mLogDir.delete();
+ }
+ }
+
+ private ScreenRecordCollector initListener() throws IOException {
+ ScreenRecordCollector listener = spy(new ScreenRecordCollector());
+ listener.setInstrumentation(mInstrumentation);
+ doReturn(mLogDir).when(listener).createAndEmptyDirectory(anyString());
+ doReturn(0L).when(listener).getTailBuffer();
+ doReturn(mDevice).when(listener).getDevice();
+ doReturn("1234").when(mDevice).executeShellCommand(eq("pidof screenrecord"));
+ doReturn("").when(mDevice).executeShellCommand(not(eq("pidof screenrecord")));
+ return listener;
+ }
+
+ /**
+ * Test that screen recording is properly started and ended for each test over the course of a
+ * test run.
+ */
+ @Test
+ public void testScreenRecord() throws Exception {
+ mListener = initListener();
+
+ // Verify output directories are created on test run start.
+ mListener.testRunStarted(mRunDesc);
+ verify(mListener).createAndEmptyDirectory(ScreenRecordCollector.OUTPUT_DIR);
+
+ // Walk through a number of test cases to simulate behavior.
+ for (int i = 1; i <= NUM_TEST_CASE; i++) {
+ // Verify a thread is started when the test starts.
+ mListener.testStarted(mTestDesc);
+ verify(mListener, times(i)).startScreenRecordThread(any());
+ // Delay verification by 100 ms to ensure the thread was started.
+ SystemClock.sleep(100);
+ // Expect all recordings to be finished because of mocked commands.
+ verify(mDevice, times(i)).executeShellCommand(matches("screenrecord .*video.mp4"));
+ for (int r = 2; r < ScreenRecordCollector.MAX_RECORDING_PARTS; r++) {
+ verify(mDevice, times(i))
+ .executeShellCommand(
+ matches(String.format("screenrecord .*video%d.mp4", r)));
+ }
+
+ // Alternate between pass and fail for variety.
+ if (i % 2 == 0) {
+ mListener.testFailure(new Failure(mTestDesc, new RuntimeException("I failed")));
+ }
+
+ // Verify all processes are killed when the test ends.
+ mListener.testFinished(mTestDesc);
+ verify(mListener, times(i)).killScreenRecordProcesses();
+ verify(mDevice, times(i)).executeShellCommand(eq("pidof screenrecord"));
+ verify(mDevice, times(i)).executeShellCommand(matches("kill -2 1234"));
+ }
+
+ // Verify files are reported
+ mListener.testRunFinished(new Result());
+
+ Bundle resultBundle = new Bundle();
+ mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+ ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+ Mockito.verify(mInstrumentation, times(NUM_TEST_CASE))
+ .sendStatus(
+ Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS),
+ capture.capture());
+ List<Bundle> capturedBundle = capture.getAllValues();
+ assertEquals(NUM_TEST_CASE, capturedBundle.size());
+
+ int videoCount = 0;
+ for (Bundle bundle : capturedBundle) {
+ for (String key : bundle.keySet()) {
+ if (key.contains("mp4")) videoCount++;
+ }
+ }
+ assertEquals(NUM_TEST_CASE * ScreenRecordCollector.MAX_RECORDING_PARTS, videoCount);
+ }
+
+ /** Test that screen recording is properly done for multiple tests and labels iterations. */
+ @Test
+ public void testScreenRecord_multipleTests() throws Exception {
+ mListener = initListener();
+
+ // Run through a sequence of `NUM_TEST_CASE` failing tests.
+ mListener.testRunStarted(mRunDesc);
+
+ // Walk through a number of test cases to simulate behavior.
+ for (int i = 1; i <= NUM_TEST_CASE; i++) {
+ mListener.testStarted(mTestDesc);
+ SystemClock.sleep(100);
+ mListener.testFinished(mTestDesc);
+ }
+ mListener.testRunFinished(new Result());
+
+ // Verify that videos are saved with iterations.
+ InOrder videoVerifier = inOrder(mDevice);
+ // The first video should not have an iteration number.
+ videoVerifier
+ .verify(mDevice, times(ScreenRecordCollector.MAX_RECORDING_PARTS))
+ .executeShellCommand(matches("^.*[^1]-video.*.mp4$"));
+ // The subsequent videos should have an iteration number.
+ for (int i = 1; i < NUM_TEST_CASE; i++) {
+ videoVerifier
+ .verify(mDevice)
+ .executeShellCommand(endsWith(String.format("%d-video.mp4", i + 1)));
+ // Verify the iteration-specific and part-specific interactions too.
+ for (int p = 2; p <= ScreenRecordCollector.MAX_RECORDING_PARTS; p++) {
+ videoVerifier
+ .verify(mDevice)
+ .executeShellCommand(endsWith(String.format("%d-video%d.mp4", i + 1, p)));
+ }
+ }
+ }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
similarity index 61%
rename from libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java
rename to libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
index 883a058..ec12f3b 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
@@ -16,16 +16,17 @@
package android.device.collectors;
-import static android.device.collectors.RssSnapshotListener.DROP_CACHE_KEY;
-import static android.device.collectors.RssSnapshotListener.OUTPUT_DIR_KEY;
-import static android.device.collectors.RssSnapshotListener.PROCESS_NAMES_KEY;
-import static android.device.collectors.RssSnapshotListener.PROCESS_SEPARATOR;
+import static android.device.collectors.ShowmapSnapshotListener.DROP_CACHE_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.METRIC_NAME_INDEX;
+import static android.device.collectors.ShowmapSnapshotListener.OUTPUT_DIR_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.PROCESS_NAMES_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.PROCESS_SEPARATOR;
import static org.mockito.Mockito.verify;
import android.app.Instrumentation;
import android.os.Bundle;
import androidx.test.runner.AndroidJUnit4;
-import com.android.helpers.RssSnapshotHelper;
+import com.android.helpers.ShowmapSnapshotHelper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.Description;
@@ -34,20 +35,21 @@
import org.mockito.MockitoAnnotations;
/**
- * Android Unit tests for {@link RssSnapshotListener}.
+ * Android Unit tests for {@link ShowmapSnapshotListener}.
*
* To run:
- * atest CollectorDeviceLibTest:android.device.collectors.RssSnapshotListenerTest
+ * atest CollectorDeviceLibTest:android.device.collectors.ShowmapSnapshotListenerTest
*/
@RunWith(AndroidJUnit4.class)
-public class RssSnapshotListenerTest {
+public class ShowmapSnapshotListenerTest {
@Mock private Instrumentation mInstrumentation;
- @Mock private RssSnapshotHelper mRssSnapshotHelper;
+ @Mock private ShowmapSnapshotHelper mShowmapSnapshotHelper;
- private RssSnapshotListener mListener;
+ private ShowmapSnapshotListener mListener;
private Description mRunDesc;
private static final String VALID_OUTPUT_DIR = "/data/local/tmp";
+ private static final String SAMPLE_METRIC_INDEX = "rss:1,pss:2";
@Before
public void setUp() {
@@ -55,8 +57,8 @@
mRunDesc = Description.createSuiteDescription("run");
}
- private RssSnapshotListener initListener(Bundle b) {
- RssSnapshotListener listener = new RssSnapshotListener(b, mRssSnapshotHelper);
+ private ShowmapSnapshotListener initListener(Bundle b) {
+ ShowmapSnapshotListener listener = new ShowmapSnapshotListener(b, mShowmapSnapshotHelper);
listener.setInstrumentation(mInstrumentation);
return listener;
}
@@ -71,21 +73,23 @@
mListener.testRunStarted(mRunDesc);
- verify(mRssSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1", "process2");
+ verify(mShowmapSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1", "process2");
}
@Test
public void testAdditionalOptions() throws Exception {
Bundle b = new Bundle();
b.putString(PROCESS_NAMES_KEY, "process1");
+ b.putString(METRIC_NAME_INDEX, "rss:1,pss:2");
b.putString(OUTPUT_DIR_KEY, VALID_OUTPUT_DIR);
b.putString(DROP_CACHE_KEY, "all");
mListener = initListener(b);
mListener.testRunStarted(mRunDesc);
- verify(mRssSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1");
+ verify(mShowmapSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1");
+ verify(mShowmapSnapshotHelper).setMetricNameIndex(SAMPLE_METRIC_INDEX);
// DROP_CACHE_KEY values: "pagecache" = 1, "slab" = 2, "all" = 3
- verify(mRssSnapshotHelper).setDropCacheOption(3);
+ verify(mShowmapSnapshotHelper).setDropCacheOption(3);
}
}
diff --git a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
index 731f3de..db8606d 100644
--- a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
+++ b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
@@ -17,6 +17,7 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
@@ -96,6 +97,9 @@
// Stub calls to permission APIs.
doNothing().when(mListener).adoptShellPermissionIdentity();
doNothing().when(mListener).dropShellPermissionIdentity();
+ // Stub calls to StatsLog APIs.
+ doReturn(true).when(mListener).logStart(anyInt());
+ doReturn(true).when(mListener).logStop(anyInt());
// Stub file I/O.
doAnswer(invocation -> invocation.getArgument(0)).when(mListener).writeToFile(any(), any());
// Stub randome UUID generation.
@@ -116,8 +120,10 @@
mListener.onTestRunStart(runData, description);
verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_1), eq(CONFIG_1.toByteArray()));
verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(CONFIG_2.toByteArray()));
+ verify(mListener, times(1)).logStart(eq(StatsdListener.RUN_EVENT_LABEL));
mListener.onTestRunEnd(runData, new Result());
+ verify(mListener, times(1)).logStop(eq(StatsdListener.RUN_EVENT_LABEL));
verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_1));
verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_2));
verify(mListener, times(1)).removeStatsConfig(eq(CONFIG_ID_1));
@@ -201,8 +207,10 @@
mListener.onTestStart(testData, description);
verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_1), eq(CONFIG_1.toByteArray()));
verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(CONFIG_2.toByteArray()));
+ verify(mListener, times(1)).logStart(eq(StatsdListener.TEST_EVENT_LABEL));
mListener.onTestEnd(testData, description);
+ verify(mListener, times(1)).logStop(eq(StatsdListener.TEST_EVENT_LABEL));
verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_1));
verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_2));
verify(mListener, times(1)).removeStatsConfig(eq(CONFIG_ID_1));
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java b/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
index ff2de41..0723221 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
@@ -16,8 +16,12 @@
package com.android.server.wm.flicker;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
+import java.util.stream.Collectors;
/**
* Collection of functional interfaces and classes representing assertions and their associated
@@ -50,14 +54,72 @@
* Utility class to store assertions with an identifier to help generate more useful debug data
* when dealing with multiple assertions.
*/
- public static class NamedAssertion<T> {
- public final TraceAssertion<T> assertion;
- public final String name;
+ public static class NamedAssertion<T> implements TraceAssertion<T> {
+ private final TraceAssertion<T> assertion;
+ private final String name;
public NamedAssertion(TraceAssertion<T> assertion, String name) {
this.assertion = assertion;
this.name = name;
}
+
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public Result apply(T t) {
+ return this.assertion.apply(t);
+ }
+
+ @Override
+ public String toString() {
+ return "Assertion(" + this.name + ")";
+ }
+ }
+
+ public static class CompoundAssertion<T> extends NamedAssertion<T> {
+ private final List<NamedAssertion<T>> assertions = new ArrayList<>();
+
+ public CompoundAssertion(TraceAssertion<T> assertion, String name) {
+ super(assertion, name);
+
+ add(assertion, name);
+ }
+
+ public void add(TraceAssertion<T> assertion, String name) {
+ this.assertions.add(new NamedAssertion<>(assertion, name));
+ }
+
+ @Override
+ public String getName() {
+ return this.assertions.stream().map(p -> p.name).collect(Collectors.joining(" and "));
+ }
+
+ @Override
+ public Result apply(T t) {
+ List<Result> assertionResults =
+ this.assertions.stream().map(p -> p.apply(t)).collect(Collectors.toList());
+
+ boolean passed = assertionResults.stream().allMatch(Result::passed);
+ String reason =
+ assertionResults
+ .stream()
+ .filter(p -> !p.passed())
+ .map(p -> p.reason)
+ .collect(Collectors.joining(" and "));
+
+ Optional<Long> timestamp = assertionResults.stream().map(p -> p.timestamp).findFirst();
+
+ return timestamp
+ .map(p -> new Result(passed, p, this.getName(), reason))
+ .orElseGet(() -> new Result(passed, reason));
+ }
+
+ @Override
+ public String toString() {
+ return "CompoundAssertion(" + this.getName() + ")";
+ }
}
/** Contains the result of an assertion including the reason for failed assertions. */
@@ -114,9 +176,13 @@
private String prettyTimestamp(long timestamp_ns) {
StringBuilder prettyTimestamp = new StringBuilder();
TimeUnit[] timeUnits = {
- TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS, TimeUnit.MILLISECONDS
+ TimeUnit.DAYS,
+ TimeUnit.HOURS,
+ TimeUnit.MINUTES,
+ TimeUnit.SECONDS,
+ TimeUnit.MILLISECONDS
};
- String[] unitSuffixes = {"h", "m", "s", "ms"};
+ String[] unitSuffixes = {"d", "h", "m", "s", "ms"};
for (int i = 0; i < timeUnits.length; i++) {
long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS);
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java b/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
index c30e012..68cc812 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
@@ -17,8 +17,8 @@
package com.android.server.wm.flicker;
import com.android.server.wm.flicker.Assertions.NamedAssertion;
+import com.android.server.wm.flicker.Assertions.CompoundAssertion;
import com.android.server.wm.flicker.Assertions.Result;
-import com.android.server.wm.flicker.Assertions.TraceAssertion;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -35,11 +35,28 @@
private boolean mFilterEntriesByRange = false;
private long mFilterStartTime = 0;
private long mFilterEndTime = 0;
+ private boolean mSkipUntilFirstAssertion = false;
private AssertionOption mOption = AssertionOption.NONE;
- private List<NamedAssertion<T>> mAssertions = new LinkedList<>();
+ private List<CompoundAssertion<T>> mAssertions = new LinkedList<>();
public void add(Assertions.TraceAssertion<T> assertion, String name) {
- mAssertions.add(new NamedAssertion<>(assertion, name));
+ mAssertions.add(new CompoundAssertion<>(assertion, name));
+ }
+
+ public void append(Assertions.TraceAssertion<T> assertion, String name) {
+ CompoundAssertion<T> lastAssertion = mAssertions.get(mAssertions.size() - 1);
+ lastAssertion.add(assertion, name);
+ }
+
+ /**
+ * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+ * end of the trace without passing any assertion, return a failure with the name/reason from
+ * the first assertion
+ *
+ * @return
+ */
+ public void skipUntilFirstAssertion() {
+ mSkipUntilFirstAssertion = true;
}
public void filterByRange(long startTime, long endTime) {
@@ -76,7 +93,6 @@
*/
public List<Result> test(List<T> entries) {
List<T> filteredEntries;
- List<Result> failures;
if (mFilterEntriesByRange) {
filteredEntries =
@@ -106,8 +122,12 @@
* added. Each assertion must be true for at least a single trace entry.
*
* <p>This can be used to check for asserting a change in property over a trace. Such as
- * visibility for a window changes from true to false or top-most window changes from A to Bb
- * and back to A again.
+ * visibility for a window changes from true to false or top-most window changes from A to B and
+ * back to A again.
+ *
+ * <p>It is also possible to ignore failures on initial elements, until the first assertion
+ * passes, this allows the trace to be recorded for longer periods, and the checks to happen
+ * only after some time.
*/
private List<Result> assertChanges(List<T> entries) {
List<Result> failures = new ArrayList<>();
@@ -120,14 +140,21 @@
}
while (assertionIndex < mAssertions.size() && entryIndex < entries.size()) {
- TraceAssertion<T> currentAssertion = mAssertions.get(assertionIndex).assertion;
+ NamedAssertion<T> currentAssertion = mAssertions.get(assertionIndex);
Result result = currentAssertion.apply(entries.get(entryIndex));
+ boolean ignoreFailure = mSkipUntilFirstAssertion && lastPassedAssertionIndex == -1;
+
if (result.passed()) {
lastPassedAssertionIndex = assertionIndex;
entryIndex++;
continue;
}
+ if (ignoreFailure) {
+ entryIndex++;
+ continue;
+ }
+
if (lastPassedAssertionIndex != assertionIndex) {
failures.add(result);
break;
@@ -140,25 +167,29 @@
}
}
+ if (lastPassedAssertionIndex == -1 && mAssertions.size() > 0 && failures.isEmpty()) {
+ failures.add(new Result(false /* success */, mAssertions.get(0).getName()));
+ }
+
if (failures.isEmpty()) {
if (assertionIndex != mAssertions.size() - 1) {
String reason =
"\nAssertion "
- + mAssertions.get(assertionIndex).name
+ + mAssertions.get(assertionIndex).getName()
+ " never became false";
reason +=
"\nPassed assertions: "
+ mAssertions
.stream()
.limit(assertionIndex)
- .map(assertion -> assertion.name)
+ .map(NamedAssertion::getName)
.collect(Collectors.joining(","));
reason +=
"\nUntested assertions: "
+ mAssertions
.stream()
.skip(assertionIndex + 1)
- .map(assertion -> assertion.name)
+ .map(NamedAssertion::getName)
.collect(Collectors.joining(","));
Result result =
@@ -176,7 +207,7 @@
private List<Result> assertEntry(T entry) {
List<Result> failures = new ArrayList<>();
for (NamedAssertion<T> assertion : mAssertions) {
- Result result = assertion.assertion.apply(entry);
+ Result result = assertion.apply(entry);
if (result.failed()) {
failures.add(result);
}
@@ -187,9 +218,7 @@
private List<Result> assertAll(List<T> entries) {
return mAssertions
.stream()
- .flatMap(
- assertion ->
- entries.stream().map(assertion.assertion).filter(Result::failed))
+ .flatMap(assertion -> entries.stream().map(assertion).filter(Result::failed))
.collect(Collectors.toList());
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
index 74faca7..f3eee72 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
@@ -44,10 +44,12 @@
public class LayersTrace {
private final List<Entry> mEntries;
@Nullable private final Path mSource;
+ @Nullable private final String mSourceChecksum;
- private LayersTrace(List<Entry> entries, Path source) {
+ private LayersTrace(List<Entry> entries, Path source, String sourceChecksum) {
this.mEntries = entries;
this.mSource = source;
+ this.mSourceChecksum = sourceChecksum;
}
/**
@@ -58,8 +60,21 @@
* @param source Path to source of data for additional debug information
* @param orphanLayerCallback a callback to handle any unexpected orphan layers
*/
- public static LayersTrace parseFrom(byte[] data, Path source,
- Consumer<Layer> orphanLayerCallback) {
+ public static LayersTrace parseFrom(
+ byte[] data, Path source, Consumer<Layer> orphanLayerCallback) {
+ return parseFrom(data, source, null /* sourceChecksum */, orphanLayerCallback);
+ }
+
+ /**
+ * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
+ * of trace entries, storing the flattened layers into its hierarchical structure.
+ *
+ * @param data binary proto data
+ * @param source Path to source of data for additional debug information
+ * @param orphanLayerCallback a callback to handle any unexpected orphan layers
+ */
+ public static LayersTrace parseFrom(
+ byte[] data, Path source, String sourceChecksum, Consumer<Layer> orphanLayerCallback) {
List<Entry> entries = new ArrayList<>();
LayersTraceFileProto fileProto;
try {
@@ -74,7 +89,18 @@
orphanLayerCallback);
entries.add(entry);
}
- return new LayersTrace(entries, source);
+ return new LayersTrace(entries, source, sourceChecksum);
+ }
+
+ /**
+ * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
+ * of trace entries, storing the flattened layers into its hierarchical structure.
+ *
+ * @param data binary proto data
+ * @param source Path to source of data for additional debug information
+ */
+ public static LayersTrace parseFrom(byte[] data, Path source, String sourceChecksum) {
+ return parseFrom(data, source, sourceChecksum, null /* orphanLayerCallback */);
}
/**
@@ -85,7 +111,7 @@
* @param source Path to source of data for additional debug information
*/
public static LayersTrace parseFrom(byte[] data, Path source) {
- return parseFrom(data, source, null /* orphanLayerCallback */);
+ return parseFrom(data, source, null /* sourceChecksum */, null /* orphanLayerCallback */);
}
/**
@@ -95,7 +121,7 @@
* @param data binary proto data
*/
public static LayersTrace parseFrom(byte[] data) {
- return parseFrom(data, null);
+ return parseFrom(data, null /* source */);
}
public List<Entry> getEntries() {
@@ -115,6 +141,10 @@
return Optional.ofNullable(mSource);
}
+ public String getSourceChecksum() {
+ return mSourceChecksum;
+ }
+
/** Represents a single Layer trace entry. */
public static class Entry implements ITraceEntry {
private long mTimestamp;
@@ -126,6 +156,33 @@
this.mRootLayers = rootLayers;
}
+ /**
+ * Determines the id of the root element.
+ *
+ * <p>On some files, such as the ones used in the FlickerLib testdata, the root nodes are
+ * those that have parent=0, on newer traces, the root nodes are those that have parent=-1
+ *
+ * <p>This function keeps compatibility with both new and older traces by searching for a
+ * known root layer (Display Root) and considering its parent Id as overall root.
+ */
+ private static Layer getRootLayer(SparseArray<Layer> layerMap) {
+ Layer knownRoot = null;
+ int numKeys = layerMap.size();
+ for (int i = 0; i < numKeys; ++i) {
+ Layer currentLayer = layerMap.valueAt(i);
+ if (currentLayer.isRootLayer()) {
+ knownRoot = currentLayer;
+ break;
+ }
+ }
+
+ if (knownRoot == null) {
+ throw new IllegalStateException("Display root layer not found.");
+ }
+
+ return layerMap.get(knownRoot.getParentId());
+ }
+
/** Constructs the layer hierarchy from a flattened list of layers. */
public static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos,
Consumer<Layer> orphanLayerCallback) {
@@ -156,13 +213,14 @@
newLayer.addParent(layerMap.get(parentId));
}
- // Remove root node (id = 0)
- orphans.remove(layerMap.get(-1));
+ // Remove root node
+ Layer rootLayer = getRootLayer(layerMap);
+ orphans.remove(rootLayer);
// Fail if we find orphan layers.
orphans.forEach(
orphan -> {
if (orphanLayerCallback != null) {
- // Workaround for b/141326137, ignore the existance of an orphan layer
+ // Workaround for b/141326137, ignore the existence of an orphan layer
orphanLayerCallback.accept(orphan);
return;
}
@@ -171,7 +229,7 @@
.stream()
.map(node -> Integer.toString(node.getId()))
.collect(Collectors.joining(", "));
- int orphanId = orphan.mChildren.get(0).mProto.parent;
+ int orphanId = orphan.mChildren.get(0).getParentId();
throw new RuntimeException(
"Failed to parse layers trace. Found orphan layers with parent "
+ "layer id:"
@@ -180,7 +238,7 @@
+ childNodes);
});
- return new Entry(timestamp, layerMap.get(-1).mChildren);
+ return new Entry(timestamp, rootLayer.mChildren);
}
/** Extracts {@link Rect} from {@link RectProto}. */
@@ -282,6 +340,22 @@
return new Result(false /* success */, this.mTimestamp, assertionName, reason);
}
+ /** Checks if a layer with name {@code layerName} exists in the hierarchy. */
+ public Result exists(String layerName) {
+ String assertionName = "exists";
+ String reason = "Could not find " + layerName;
+ for (Layer layer : asFlattenedLayers()) {
+ if (layer.mProto.name.contains(layerName)) {
+ return new Result(
+ true /* success */,
+ this.mTimestamp,
+ assertionName,
+ layer.mProto.name + " exists");
+ }
+ }
+ return new Result(false /* success */, this.mTimestamp, assertionName, reason);
+ }
+
/** Checks if a layer with name {@code layerName} is visible. */
public Result isVisible(String layerName) {
String assertionName = "isVisible";
@@ -363,6 +437,18 @@
return mProto.id;
}
+ public int getParentId() {
+ return mProto.parent;
+ }
+
+ public String getName() {
+ if (mProto != null) {
+ return mProto.name;
+ }
+
+ return "";
+ }
+
public boolean isActiveBufferEmpty() {
return this.mProto.activeBuffer == null
|| this.mProto.activeBuffer.height == 0
@@ -394,7 +480,7 @@
}
public boolean isRootLayer() {
- return mParent == null || mParent.mProto == null;
+ return mParent != null && mParent.mProto == null;
}
public boolean isInvisible() {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
index 70bcdcd..21a3f2b 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
@@ -45,6 +45,15 @@
LayersTraceSubject::new;
private AssertionsChecker<Entry> mChecker = new AssertionsChecker<>();
+ private boolean mNewAssertion = true;
+
+ private void addAssertion(Assertions.TraceAssertion<Entry> assertion, String name) {
+ if (mNewAssertion) {
+ mChecker.add(assertion, name);
+ } else {
+ mChecker.append(assertion, name);
+ }
+ }
private LayersTraceSubject(FailureMetadata fm, @Nullable LayersTrace subject) {
super(fm, subject);
@@ -70,8 +79,12 @@
// User-defined entry point
public static LayersTraceSubject assertThat(@Nullable TransitionResult result,
Consumer<LayersTrace.Layer> orphanLayerCallback) {
- LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(),
- result.getLayersTracePath(), orphanLayerCallback);
+ LayersTrace entries =
+ LayersTrace.parseFrom(
+ result.getLayersTrace(),
+ result.getLayersTracePath(),
+ result.getLayersTraceChecksum(),
+ orphanLayerCallback);
return assertWithMessage(result.toString()).about(FACTORY).that(entries);
}
@@ -90,10 +103,29 @@
}
public LayersTraceSubject then() {
+ mNewAssertion = true;
mChecker.checkChangingAssertions();
return this;
}
+ public LayersTraceSubject and() {
+ mNewAssertion = false;
+ mChecker.checkChangingAssertions();
+ return this;
+ }
+
+ /**
+ * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+ * end of the trace without passing any assertion, return a failure with the name/reason from
+ * the first assertion
+ *
+ * @return
+ */
+ public LayersTraceSubject skipUntilFirstAssertion() {
+ mChecker.skipUntilFirstAssertion();
+ return this;
+ }
+
public void inTheBeginning() {
if (actual().getEntries().isEmpty()) {
fail("No entries found.");
@@ -120,6 +152,8 @@
tracePath =
"\nLayers Trace can be found in: "
+ actual().getSource().get().toAbsolutePath()
+ + "\nChecksum: "
+ + actual().getSourceChecksum()
+ "\n";
}
fail(tracePath + failureLogs);
@@ -127,24 +161,40 @@
}
public LayersTraceSubject coversRegion(Rect rect) {
- mChecker.add(entry -> entry.coversRegion(rect), "coversRegion(" + rect + ")");
+ addAssertion(entry -> entry.coversRegion(rect), "coversRegion(" + rect + ")");
return this;
}
public LayersTraceSubject hasVisibleRegion(String layerName, Rect size) {
- mChecker.add(
+ addAssertion(
entry -> entry.hasVisibleRegion(layerName, size),
"hasVisibleRegion(" + layerName + size + ")");
return this;
}
- public LayersTraceSubject showsLayer(String layerName) {
- mChecker.add(entry -> entry.isVisible(layerName), "showsLayer(" + layerName + ")");
+ public LayersTraceSubject hasNotLayer(String layerName) {
+ addAssertion(entry -> entry.exists(layerName).negate(), "hasNotLayer(" + layerName + ")");
return this;
}
- public LayersTraceSubject hidesLayer(String layerName) {
- mChecker.add(entry -> entry.isVisible(layerName).negate(), "hidesLayer(" + layerName + ")");
+ public LayersTraceSubject hasLayer(String layerName) {
+ addAssertion(entry -> entry.exists(layerName), "hasLayer(" + layerName + ")");
return this;
}
+
+ public LayersTraceSubject showsLayer(String layerName) {
+ addAssertion(entry -> entry.isVisible(layerName), "showsLayer(" + layerName + ")");
+ return this;
+ }
+
+ public LayersTraceSubject replaceVisibleLayer(
+ String previousLayerName, String currentLayerName) {
+ return hidesLayer(previousLayerName).and().showsLayer(currentLayerName);
+ }
+
+ public LayersTraceSubject hidesLayer(String layerName) {
+ addAssertion(entry -> entry.isVisible(layerName).negate(), "hidesLayer(" + layerName + ")");
+ return this;
+ }
+
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
index 007196b..0b74a6c 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
@@ -16,7 +16,7 @@
package com.android.server.wm.flicker;
-import static com.android.server.wm.flicker.monitor.ITransitionMonitor.OUTPUT_DIR;
+import static com.android.server.wm.flicker.monitor.TransitionMonitor.OUTPUT_DIR;
import android.util.Log;
@@ -25,15 +25,13 @@
import androidx.annotation.VisibleForTesting;
import androidx.test.InstrumentationRegistry;
-import com.android.server.wm.flicker.monitor.ITransitionMonitor;
+import com.android.server.wm.flicker.monitor.TransitionMonitor;
import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
import com.android.server.wm.flicker.monitor.ScreenRecorder;
import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
-import com.google.common.io.Files;
-
-import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -101,8 +99,8 @@
private final LayersTraceMonitor mLayersTraceMonitor;
private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
- private final List<ITransitionMonitor> mAllRunsMonitors;
- private final List<ITransitionMonitor> mPerRunMonitors;
+ private final List<TransitionMonitor> mAllRunsMonitors;
+ private final List<TransitionMonitor> mPerRunMonitors;
private final List<Runnable> mBeforeAlls;
private final List<Runnable> mBefores;
private final List<Runnable> mTransitions;
@@ -148,13 +146,13 @@
*/
public TransitionRunner run() {
mResults = new ArrayList<>();
- mAllRunsMonitors.forEach(ITransitionMonitor::start);
+ mAllRunsMonitors.forEach(TransitionMonitor::start);
mBeforeAlls.forEach(Runnable::run);
for (int iteration = 0; iteration < mIterations; iteration++) {
mBefores.forEach(Runnable::run);
- mPerRunMonitors.forEach(ITransitionMonitor::start);
+ mPerRunMonitors.forEach(TransitionMonitor::start);
mTransitions.forEach(Runnable::run);
- mPerRunMonitors.forEach(ITransitionMonitor::stop);
+ mPerRunMonitors.forEach(TransitionMonitor::stop);
mAfters.forEach(Runnable::run);
if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) {
String msg =
@@ -211,19 +209,31 @@
*/
private TransitionResult saveResult(int iteration) {
Path windowTrace = null;
+ String windowTraceChecksum = "";
Path layerTrace = null;
+ String layerTraceChecksum = "";
Path screenCaptureVideo = null;
+ String screenCaptureVideoChecksum = "";
if (mPerRunMonitors.contains(mWmTraceMonitor)) {
windowTrace = mWmTraceMonitor.save(mTestTag, iteration);
+ windowTraceChecksum = mWmTraceMonitor.getChecksum();
}
if (mPerRunMonitors.contains(mLayersTraceMonitor)) {
layerTrace = mLayersTraceMonitor.save(mTestTag, iteration);
+ layerTraceChecksum = mLayersTraceMonitor.getChecksum();
}
if (mPerRunMonitors.contains(mScreenRecorder)) {
screenCaptureVideo = mScreenRecorder.save(mTestTag, iteration);
+ screenCaptureVideoChecksum = mScreenRecorder.getChecksum();
}
- return new TransitionResult(layerTrace, windowTrace, screenCaptureVideo);
+ return new TransitionResult(
+ layerTrace,
+ layerTraceChecksum,
+ windowTrace,
+ windowTraceChecksum,
+ screenCaptureVideo,
+ screenCaptureVideoChecksum);
}
private boolean runJankFree() {
@@ -237,18 +247,27 @@
/** Stores paths to all test artifacts. */
@VisibleForTesting
public static class TransitionResult {
- @Nullable public final Path layersTrace;
- @Nullable public final Path windowManagerTrace;
- @Nullable public final Path screenCaptureVideo;
+ @Nullable private final Path layersTrace;
+ private final String layersTraceChecksum;
+ @Nullable private final Path windowManagerTrace;
+ private final String windowManagerTraceChecksum;
+ @Nullable private final Path screenCaptureVideo;
+ private final String screenCaptureVideoChecksum;
private boolean flaggedForSaving = true;
public TransitionResult(
@Nullable Path layersTrace,
+ String layersTraceChecksum,
@Nullable Path windowManagerTrace,
- @Nullable Path screenCaptureVideo) {
+ String windowManagerTraceChecksum,
+ @Nullable Path screenCaptureVideo,
+ String screenCaptureVideoChecksum) {
this.layersTrace = layersTrace;
+ this.layersTraceChecksum = layersTraceChecksum;
this.windowManagerTrace = windowManagerTrace;
+ this.windowManagerTraceChecksum = windowManagerTraceChecksum;
this.screenCaptureVideo = screenCaptureVideo;
+ this.screenCaptureVideoChecksum = screenCaptureVideoChecksum;
}
public void flagForSaving() {
@@ -265,8 +284,8 @@
public byte[] getLayersTrace() {
try {
- return Files.toByteArray(this.layersTrace.toFile());
- } catch (IOException e) {
+ return Files.readAllBytes(this.layersTrace);
+ } catch (Exception e) {
throw new RuntimeException(e);
}
}
@@ -275,14 +294,18 @@
return layersTrace;
}
+ public String getLayersTraceChecksum() {
+ return layersTraceChecksum;
+ }
+
public boolean windowManagerTraceExists() {
return windowManagerTrace != null && windowManagerTrace.toFile().exists();
}
public byte[] getWindowManagerTrace() {
try {
- return Files.toByteArray(this.windowManagerTrace.toFile());
- } catch (IOException e) {
+ return Files.readAllBytes(this.windowManagerTrace);
+ } catch (Exception e) {
throw new RuntimeException(e);
}
}
@@ -291,6 +314,10 @@
return windowManagerTrace;
}
+ public String getWindowManagerTraceChecksum() {
+ return windowManagerTraceChecksum;
+ }
+
public boolean screenCaptureVideoExists() {
return screenCaptureVideo != null && screenCaptureVideo.toFile().exists();
}
@@ -299,6 +326,10 @@
return screenCaptureVideo;
}
+ public String getScreenCaptureVideoChecksum() {
+ return screenCaptureVideoChecksum;
+ }
+
public void delete() {
if (layersTraceExists()) layersTrace.toFile().delete();
if (windowManagerTraceExists()) windowManagerTrace.toFile().delete();
@@ -313,8 +344,8 @@
private LayersTraceMonitor mLayersTraceMonitor;
private WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
- private List<ITransitionMonitor> mAllRunsMonitors = new LinkedList<>();
- private List<ITransitionMonitor> mPerRunMonitors = new LinkedList<>();
+ private List<TransitionMonitor> mAllRunsMonitors = new LinkedList<>();
+ private List<TransitionMonitor> mPerRunMonitors = new LinkedList<>();
private List<Runnable> mBeforeAlls = new LinkedList<>();
private List<Runnable> mBefores = new LinkedList<>();
private List<Runnable> mTransitions = new LinkedList<>();
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
index 3afecc9..69b5fef 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
@@ -44,10 +44,12 @@
private static final int DEFAULT_DISPLAY = 0;
private final List<Entry> mEntries;
@Nullable private final Path mSource;
+ @Nullable private final String mSourceChecksum;
- private WindowManagerTrace(List<Entry> entries, Path source) {
+ private WindowManagerTrace(List<Entry> entries, Path source, String sourceChecksum) {
this.mEntries = entries;
this.mSource = source;
+ this.mSourceChecksum = sourceChecksum;
}
/**
@@ -57,7 +59,7 @@
* @param data binary proto data
* @param source Path to source of data for additional debug information
*/
- public static WindowManagerTrace parseFrom(byte[] data, Path source) {
+ public static WindowManagerTrace parseFrom(byte[] data, Path source, String checksum) {
List<Entry> entries = new ArrayList<>();
WindowManagerTraceFileProto fileProto;
@@ -69,11 +71,11 @@
for (WindowManagerTraceProto entryProto : fileProto.entry) {
entries.add(new Entry(entryProto));
}
- return new WindowManagerTrace(entries, source);
+ return new WindowManagerTrace(entries, source, checksum);
}
public static WindowManagerTrace parseFrom(byte[] data) {
- return parseFrom(data, null);
+ return parseFrom(data, null /* source */, null /* checksum */);
}
public List<Entry> getEntries() {
@@ -93,6 +95,10 @@
return Optional.ofNullable(mSource);
}
+ public String getSourceChecksum() {
+ return mSourceChecksum;
+ }
+
/** Represents a single WindowManager trace entry. */
public static class Entry implements ITraceEntry {
private final WindowManagerTraceProto mProto;
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
index 21a2606..c44e0b2 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
@@ -39,6 +39,16 @@
WmTraceSubject::new;
private AssertionsChecker<WindowManagerTrace.Entry> mChecker = new AssertionsChecker<>();
+ private boolean mNewAssertion = true;
+
+ private void addAssertion(
+ Assertions.TraceAssertion<WindowManagerTrace.Entry> assertion, String name) {
+ if (mNewAssertion) {
+ mChecker.add(assertion, name);
+ } else {
+ mChecker.append(assertion, name);
+ }
+ }
private WmTraceSubject(FailureMetadata fm, @Nullable WindowManagerTrace subject) {
super(fm, subject);
@@ -53,7 +63,9 @@
public static WmTraceSubject assertThat(@Nullable TransitionResult result) {
WindowManagerTrace entries =
WindowManagerTrace.parseFrom(
- result.getWindowManagerTrace(), result.getWindowManagerTracePath());
+ result.getWindowManagerTrace(),
+ result.getWindowManagerTracePath(),
+ result.getWindowManagerTraceChecksum());
return assertWithMessage(result.toString()).about(FACTORY).that(entries);
}
@@ -72,12 +84,31 @@
}
public WmTraceSubject then() {
+ mNewAssertion = true;
mChecker.checkChangingAssertions();
return this;
}
+ public WmTraceSubject and() {
+ mNewAssertion = false;
+ mChecker.checkChangingAssertions();
+ return this;
+ }
+
+ /**
+ * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+ * end of the trace without passing any assertion, return a failure with the name/reason from
+ * the first assertion
+ *
+ * @return
+ */
+ public WmTraceSubject skipUntilFirstAssertion() {
+ mChecker.skipUntilFirstAssertion();
+ return this;
+ }
+
public void inTheBeginning() {
- if (getSubject().getEntries().isEmpty()) {
+ if (actual().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkFirstEntry();
@@ -85,7 +116,7 @@
}
public void atTheEnd() {
- if (getSubject().getEntries().isEmpty()) {
+ if (actual().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkLastEntry();
@@ -93,9 +124,9 @@
}
private void test() {
- List<Result> failures = mChecker.test(getSubject().getEntries());
+ List<Result> failures = mChecker.test(actual().getEntries());
if (!failures.isEmpty()) {
- Optional<Path> failureTracePath = getSubject().getSource();
+ Optional<Path> failureTracePath = actual().getSource();
String failureLogs =
failures.stream().map(Result::toString).collect(Collectors.joining("\n"));
String tracePath = "";
@@ -103,6 +134,8 @@
tracePath =
"\nWindowManager Trace can be found in: "
+ failureTracePath.get().toAbsolutePath()
+ + "\nChecksum: "
+ + actual().getSourceChecksum()
+ "\n";
}
fail(tracePath + failureLogs);
@@ -110,49 +143,49 @@
}
public WmTraceSubject showsAboveAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isAboveAppWindowVisible(partialWindowTitle),
"showsAboveAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesAboveAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isAboveAppWindowVisible(partialWindowTitle).negate(),
"hidesAboveAppWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsBelowAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isBelowAppWindowVisible(partialWindowTitle),
"showsBelowAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesBelowAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isBelowAppWindowVisible(partialWindowTitle).negate(),
"hidesBelowAppWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsImeWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isImeWindowVisible(partialWindowTitle),
"showsBelowAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesImeWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isImeWindowVisible(partialWindowTitle).negate(),
"hidesImeWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsAppWindowOnTop(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> {
Result result = entry.isAppWindowVisible(partialWindowTitle);
if (result.passed()) {
@@ -165,7 +198,7 @@
}
public WmTraceSubject hidesAppWindowOnTop(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> {
Result result = entry.isAppWindowVisible(partialWindowTitle).negate();
if (result.failed()) {
@@ -178,14 +211,14 @@
}
public WmTraceSubject showsAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isAppWindowVisible(partialWindowTitle),
"showsAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesAppWindow(String partialWindowTitle) {
- mChecker.add(
+ addAssertion(
entry -> entry.isAppWindowVisible(partialWindowTitle).negate(),
"hidesAppWindow(" + partialWindowTitle + ")");
return this;
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java
deleted file mode 100644
index e092d0b..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2018 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.server.wm.flicker.monitor;
-
-import android.os.Environment;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/** Collects test artifacts during a UI transition. */
-public interface ITransitionMonitor {
- Path OUTPUT_DIR = Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker");
-
- /** Starts monitor. */
- void start();
-
- /** Stops monitor. */
- void stop();
-
- /**
- * Saves any monitor artifacts to file adding {@code testTag} and {@code iteration} to the file
- * name.
- *
- * @param testTag suffix added to artifact name
- * @param iteration suffix added to artifact name
- * @return Path to saved artifact
- */
- default Path save(String testTag, int iteration) {
- return save(testTag + "_" + iteration);
- }
-
- /**
- * Saves any monitor artifacts to file adding {@code testTag} to the file name.
- *
- * @param testTag suffix added to artifact name
- * @return Path to saved artifact
- */
- default Path save(String testTag) {
- throw new UnsupportedOperationException("Save not implemented for this monitor");
- }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
index 03cf2d4..0130931 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
@@ -24,6 +24,7 @@
/** Captures Layers trace from SurfaceFlinger. */
public class LayersTraceMonitor extends TraceMonitor {
+ private static final String TRACE_FILE = "layers_trace.pb";
private IWindowManager mWm = WindowManagerGlobal.getWindowManagerService();
public LayersTraceMonitor() {
@@ -31,7 +32,7 @@
}
public LayersTraceMonitor(Path outputDir) {
- super(outputDir, "layers_trace.pb");
+ super(outputDir, TRACE_FILE);
}
@Override
@@ -45,7 +46,7 @@
}
@Override
- public boolean isEnabled() throws RemoteException {
+ public boolean isEnabled() {
try {
return mWm.isLayerTracing();
} catch (RemoteException e) {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
index 8b138aa..3a7c012 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
@@ -18,51 +18,46 @@
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
-
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
/** Captures screen contents and saves it as a mp4 video file. */
-public class ScreenRecorder implements ITransitionMonitor {
- @VisibleForTesting
- public static final Path DEFAULT_OUTPUT_PATH = OUTPUT_DIR.resolve("transition.mp4");
-
- private static final String TAG = "FLICKER";
+public class ScreenRecorder extends TraceMonitor {
+ private static final String TRACE_FILE = "transition.mp4";
private int mWidth;
private int mHeight;
private Thread mRecorderThread;
public ScreenRecorder() {
- this(720, 1280);
+ this(720, 1280, OUTPUT_DIR, TRACE_FILE);
}
- public ScreenRecorder(int width, int height) {
+ public ScreenRecorder(int width, int height, Path outputPath, String traceFile) {
+ super(outputPath, outputPath.resolve(traceFile));
mWidth = width;
mHeight = height;
}
@VisibleForTesting
- public static Path getPath(String testTag) {
- return OUTPUT_DIR.resolve(testTag + ".mp4");
+ public Path getPath() {
+ return mOutputPath;
}
@Override
public void start() {
- OUTPUT_DIR.toFile().mkdirs();
+ mOutputPath.toFile().mkdirs();
String command =
String.format(
Locale.getDefault(),
"screenrecord --size %dx%d %s",
mWidth,
mHeight,
- DEFAULT_OUTPUT_PATH);
+ mTraceFile);
mRecorderThread =
new Thread(
() -> {
@@ -86,18 +81,7 @@
}
@Override
- public Path save(String testTag) {
- if (!Files.exists(DEFAULT_OUTPUT_PATH)) {
- Log.w(TAG, "No video file found on " + DEFAULT_OUTPUT_PATH);
- return null;
- }
-
- try {
- Path targetPath = Files.move(DEFAULT_OUTPUT_PATH, getPath(testTag), REPLACE_EXISTING);
- Log.i(TAG, "Video saved to " + targetPath.toString());
- return targetPath;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ public boolean isEnabled() {
+ return mRecorderThread != null && mRecorderThread.isAlive();
}
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
index 333d723..778db73 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
@@ -23,65 +23,50 @@
import androidx.annotation.VisibleForTesting;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Locale;
/**
* Base class for monitors containing common logic to read the trace as a byte array and save the
* trace to another location.
*/
-public abstract class TraceMonitor implements ITransitionMonitor {
- public static final String TAG = "FLICKER";
- private static final String TRACE_DIR = "/data/misc/wmtrace/";
+public abstract class TraceMonitor extends TransitionMonitor {
+ private static final Path TRACE_DIR = Paths.get("/data/misc/wmtrace/");
- private Path mOutputDir;
- public String mTraceFileName;
+ protected Path mTraceFile;
public abstract boolean isEnabled() throws RemoteException;
- public TraceMonitor(Path outputDir, String traceFileName) {
- mOutputDir = outputDir;
- mTraceFileName = traceFileName;
+ TraceMonitor(Path outputDir, Path traceFile) {
+ mTraceFile = traceFile;
+ mOutputPath = outputDir;
}
- /**
- * Saves trace file to the external storage directory suffixing the name with the testtag and
- * iteration.
- *
- * <p>Moves the trace file from the default location via a shell command since the test app does
- * not have security privileges to access /data/misc/wmtrace.
- *
- * @param testTag suffix added to trace name used to identify trace
- * @return Path to saved trace file
- */
- @Override
- public Path save(String testTag) {
- mOutputDir.toFile().mkdirs();
- Path traceFileCopy = getOutputTraceFilePath(testTag);
+ TraceMonitor(Path outputDir, String traceFileName) {
+ this(outputDir, TRACE_DIR.resolve(traceFileName));
+ }
- // Move the trace file to the output directory
+ protected Path saveTrace(String testTag) {
+ Path traceFileCopy = getOutputTraceFilePath(testTag);
+ moveFile(mTraceFile, traceFileCopy);
+
+ return traceFileCopy;
+ }
+
+ protected void moveFile(Path src, Path dst) {
+ // Move the file to the output directory
// Note: Due to b/141386109, certain devices do not allow moving the files between
// directories with different encryption policies, so manually copy and then
// remove the original file
String copyCommand =
- String.format(
- Locale.getDefault(),
- "cp %s%s %s",
- TRACE_DIR,
- mTraceFileName,
- traceFileCopy.toString());
+ String.format(Locale.getDefault(), "cp %s %s", src.toString(), dst.toString());
runShellCommand(copyCommand);
- String removeCommand =
- String.format(
- Locale.getDefault(),
- "rm %s%s",
- TRACE_DIR,
- mTraceFileName);
+ String removeCommand = String.format(Locale.getDefault(), "rm %s", src.toString());
runShellCommand(removeCommand);
- return traceFileCopy;
}
@VisibleForTesting
public Path getOutputTraceFilePath(String testTag) {
- return mOutputDir.resolve(mTraceFileName + "_" + testTag);
+ return mOutputPath.resolve(testTag + "_" + mTraceFile.getFileName());
}
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java
new file mode 100644
index 0000000..ea64d33
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 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.server.wm.flicker.monitor;
+
+import android.os.Environment;
+
+import com.google.common.io.BaseEncoding;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Collects test artifacts during a UI transition. */
+public abstract class TransitionMonitor {
+ static final String TAG = "FLICKER";
+ public static final Path OUTPUT_DIR =
+ Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker");
+ protected String mChecksum;
+ protected Path mOutputPath;
+
+ /** Starts monitor. */
+ public abstract void start();
+
+ /** Stops monitor. */
+ public abstract void stop();
+
+ /**
+ * Saves any monitor artifacts to file adding {@code testTag} and {@code iteration} to the file
+ * name.
+ *
+ * @param testTag suffix added to artifact name
+ * @param iteration suffix added to artifact name
+ * @return Path to saved artifact
+ */
+ public Path save(String testTag, int iteration) {
+ return save(testTag + "_" + iteration);
+ }
+
+ /**
+ * Saves any monitor artifacts to file adding {@code testTag} to the file name.
+ *
+ * @param testTag suffix added to artifact name
+ * @return Path to saved artifact
+ */
+ /**
+ * Saves trace file to the external storage directory suffixing the name with the testtag and
+ * iteration.
+ *
+ * <p>Moves the trace file from the default location via a shell command since the test app does
+ * not have security privileges to access /data/misc/wmtrace.
+ *
+ * @param testTag suffix added to trace name used to identify trace
+ * @return Path to saved trace file and file checksum (SHA-256)
+ */
+ public Path save(String testTag) {
+ mOutputPath.toFile().mkdirs();
+ Path savedTrace = saveTrace(testTag);
+ mChecksum = calculateChecksum(savedTrace);
+ return savedTrace;
+ }
+
+ protected Path saveTrace(String testTag) {
+ throw new UnsupportedOperationException("Save not implemented for this monitor");
+ }
+
+ public String getChecksum() {
+ return mChecksum;
+ }
+
+ static String calculateChecksum(Path traceFile) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] fileData = Files.readAllBytes(traceFile);
+ byte[] hash = digest.digest(fileData);
+ return BaseEncoding.base16().encode(hash).toLowerCase();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Checksum algorithm SHA-256 not found", e);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("File not found", e);
+ }
+ }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
index 4c950a4..c1fab77 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
@@ -28,7 +28,7 @@
* <p>Adapted from {@link androidx.test.jank.internal.WindowAnimationFrameStatsMonitorImpl} using
* the same threshold to determine jank.
*/
-public class WindowAnimationFrameStatsMonitor implements ITransitionMonitor {
+public class WindowAnimationFrameStatsMonitor extends TransitionMonitor {
private static final String TAG = "FLICKER";
// Maximum normalized error in frame duration before the frame is considered janky
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
index f2be413..c216170 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
@@ -24,6 +24,7 @@
/** Captures WindowManager trace from WindowManager. */
public class WindowManagerTraceMonitor extends TraceMonitor {
+ private static final String TRACE_FILE = "wm_trace.pb";
private IWindowManager mWm = WindowManagerGlobal.getWindowManagerService();
public WindowManagerTraceMonitor() {
@@ -31,7 +32,7 @@
}
public WindowManagerTraceMonitor(Path outputDir) {
- super(outputDir, "wm_trace.pb");
+ super(outputDir, TRACE_FILE);
}
@Override
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_root.pb b/libraries/flicker/test/assets/testdata/layers_trace_root.pb
new file mode 100644
index 0000000..d961714
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_root.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb b/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb
new file mode 100644
index 0000000..666b328
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb
Binary files differ
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
index 4c6b229..6974d60 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
@@ -163,6 +163,22 @@
assertThat(failures).hasSize(1);
}
+ @Test
+ public void canFailCheckChangingAssertions_ifUsingCompoundAssertion() {
+ AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
+ checker.add(SimpleEntry::isData42, "isData42");
+ checker.append(SimpleEntry::isData0, "isData0");
+ checker.checkChangingAssertions();
+
+ List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
+
+ assertThat(failures).hasSize(1);
+ assertThat(failures.get(0).assertionName).contains("isData42");
+ assertThat(failures.get(0).assertionName).contains("isData0");
+ assertThat(failures.get(0).reason).contains("!is42");
+ assertThat(failures.get(0).reason).doesNotContain("!is0");
+ }
+
static class SimpleEntry implements ITraceEntry {
long mTimestamp;
int mData;
@@ -178,11 +194,11 @@
}
Result isData42() {
- return new Result(this.mData == 42, this.mTimestamp, "is42", "");
+ return new Result(this.mData == 42, this.mTimestamp, "is42", "!is42");
}
Result isData0() {
- return new Result(this.mData == 0, this.mTimestamp, "is42", "");
+ return new Result(this.mData == 0, this.mTimestamp, "is42", "!is0");
}
}
}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
index 0415f81..2ccae42 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
@@ -33,6 +33,8 @@
import org.junit.runners.MethodSorters;
import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
/**
* Contains {@link LayersTraceSubject} tests. To run this test: {@code atest
@@ -89,7 +91,9 @@
assertWithMessage("Contains path to trace")
.that(e.getMessage())
.contains("layers_trace_invalid_layer_visibility.pb");
- assertWithMessage("Contains timestamp").that(e.getMessage()).contains("70h13m14s303ms");
+ assertWithMessage("Contains timestamp")
+ .that(e.getMessage())
+ .contains("2d22h13m14s303ms");
assertWithMessage("Contains assertion function")
.that(e.getMessage())
.contains("!isVisible");
@@ -100,4 +104,37 @@
+ ".SimpleActivity#0 is visible");
}
}
+
+ private void detectRootLayer(String fileName) {
+ LayersTrace layersTrace = readLayerTraceFromFile(fileName);
+
+ for (LayersTrace.Entry entry : layersTrace.getEntries()) {
+ List<LayersTrace.Layer> flattened = entry.asFlattenedLayers();
+ List<LayersTrace.Layer> rootLayers =
+ flattened
+ .stream()
+ .filter(LayersTrace.Layer::isRootLayer)
+ .collect(Collectors.toList());
+
+ assertWithMessage("Does not have any root layer")
+ .that(rootLayers.size())
+ .isGreaterThan(0);
+
+ int firstParentId = rootLayers.get(0).getParentId();
+
+ assertWithMessage("Has multiple root layers")
+ .that(rootLayers.stream().allMatch(p -> p.getParentId() == firstParentId))
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void testCanDetectRootLayer() {
+ detectRootLayer("layers_trace_root.pb");
+ }
+
+ @Test
+ public void testCanDetectRootLayerAOSP() {
+ detectRootLayer("layers_trace_root_aosp.pb");
+ }
}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
index d75c58a..10562d9 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
@@ -45,7 +45,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
@@ -58,7 +57,7 @@
@Mock private WindowManagerTraceMonitor mWindowManagerTraceMonitorMock;
@Mock private LayersTraceMonitor mLayersTraceMonitorMock;
@Mock private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
- @InjectMocks private TransitionBuilder mTransitionBuilder;
+ @InjectMocks private TransitionBuilder mTransitionBuilder = TransitionRunner.newBuilder();
@Before
public void init() {
@@ -165,6 +164,7 @@
orderVerifier
.verify(mWindowManagerTraceMonitorMock)
.save("mCaptureWmTraceTransitionRunner", 0);
+ orderVerifier.verify(mWindowManagerTraceMonitorMock).getChecksum();
verifyNoMoreInteractions(mWindowManagerTraceMonitorMock);
}
@@ -183,6 +183,7 @@
orderVerifier
.verify(mLayersTraceMonitorMock)
.save("mCaptureLayersTraceTransitionRunner", 0);
+ orderVerifier.verify(mLayersTraceMonitorMock).getChecksum();
verifyNoMoreInteractions(mLayersTraceMonitorMock);
}
@@ -202,14 +203,16 @@
orderVerifier.verify(mScreenRecorderMock).start();
orderVerifier.verify(mScreenRecorderMock).stop();
orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 0);
+ orderVerifier.verify(mScreenRecorderMock).getChecksum();
orderVerifier.verify(mScreenRecorderMock).start();
orderVerifier.verify(mScreenRecorderMock).stop();
orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 1);
+ orderVerifier.verify(mScreenRecorderMock).getChecksum();
verifyNoMoreInteractions(mScreenRecorderMock);
}
@Test
- public void canRecordAllRuns() throws IOException {
+ public void canRecordAllRuns() {
doReturn(
Paths.get(
Environment.getExternalStorageDirectory().getAbsolutePath(),
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
index f349217..849e593 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
@@ -35,6 +35,7 @@
import org.junit.runners.MethodSorters;
import java.io.File;
+import java.nio.file.Path;
/**
* Contains {@link LayersTraceMonitor} tests. To run this test: {@code atest
@@ -74,8 +75,11 @@
public void captureLayersTrace() throws Exception {
mLayersTraceMonitor.start();
mLayersTraceMonitor.stop();
- File testFile = mLayersTraceMonitor.save("captureLayersTrace").toFile();
+ Path testFilePath = mLayersTraceMonitor.save("captureWindowTrace");
+ File testFile = testFilePath.toFile();
assertThat(testFile.exists()).isTrue();
+ String calculatedChecksum = TransitionMonitor.calculateChecksum(testFilePath);
+ assertThat(calculatedChecksum).isEqualTo(mLayersTraceMonitor.getChecksum());
byte[] trace = Files.toByteArray(testFile);
assertThat(trace.length).isGreaterThan(0);
LayersTraceFileProto mLayerTraceFileProto = LayersTraceFileProto.parseFrom(trace);
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
index 967f565..603590e 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
@@ -18,9 +18,6 @@
import static android.os.SystemClock.sleep;
-import static com.android.server.wm.flicker.monitor.ScreenRecorder.DEFAULT_OUTPUT_PATH;
-import static com.android.server.wm.flicker.monitor.ScreenRecorder.getPath;
-
import static com.google.common.truth.Truth.assertThat;
import androidx.test.runner.AndroidJUnit4;
@@ -33,6 +30,8 @@
import org.junit.runners.MethodSorters;
import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
/**
* Contains {@link ScreenRecorder} tests. To run this test: {@code atest
@@ -43,6 +42,7 @@
public class ScreenRecorderTest {
private static final String TEST_VIDEO_FILENAME = "test.mp4";
private ScreenRecorder mScreenRecorder;
+ private Path mSavedVideoPath = null;
@Before
public void setup() {
@@ -51,8 +51,10 @@
@After
public void teardown() {
- DEFAULT_OUTPUT_PATH.toFile().delete();
- getPath(TEST_VIDEO_FILENAME).toFile().delete();
+ mScreenRecorder.getPath().toFile().delete();
+ if (mSavedVideoPath != null) {
+ mSavedVideoPath.toFile().delete();
+ }
}
@Test
@@ -60,7 +62,7 @@
mScreenRecorder.start();
sleep(100);
mScreenRecorder.stop();
- File file = DEFAULT_OUTPUT_PATH.toFile();
+ File file = mScreenRecorder.getPath().toFile();
assertThat(file.exists()).isTrue();
}
@@ -69,8 +71,7 @@
mScreenRecorder.start();
sleep(100);
mScreenRecorder.stop();
- mScreenRecorder.save(TEST_VIDEO_FILENAME);
- File file = getPath(TEST_VIDEO_FILENAME).toFile();
- assertThat(file.exists()).isTrue();
+ Path file = mScreenRecorder.save(TEST_VIDEO_FILENAME);
+ assertThat(Files.exists(file)).isTrue();
}
}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
index 034de4c..b234703 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
@@ -35,6 +35,7 @@
import org.junit.runners.MethodSorters;
import java.io.File;
+import java.nio.file.Path;
/**
* Contains {@link WindowManagerTraceMonitor} tests. To run this test: {@code atest
@@ -74,8 +75,11 @@
public void captureWindowTrace() throws Exception {
mWindowManagerTraceMonitor.start();
mWindowManagerTraceMonitor.stop();
- File testFile = mWindowManagerTraceMonitor.save("captureWindowTrace").toFile();
+ Path testFilePath = mWindowManagerTraceMonitor.save("captureWindowTrace");
+ File testFile = testFilePath.toFile();
assertThat(testFile.exists()).isTrue();
+ String calculatedChecksum = TransitionMonitor.calculateChecksum(testFilePath);
+ assertThat(calculatedChecksum).isEqualTo(mWindowManagerTraceMonitor.getChecksum());
byte[] trace = Files.toByteArray(testFile);
assertThat(trace.length).isGreaterThan(0);
WindowManagerTraceFileProto mWindowTraceFileProto =
diff --git a/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java b/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
index 44f0e1c..b5a6bf7 100644
--- a/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
@@ -15,9 +15,16 @@
*/
package android.platform.test.rule;
+import android.support.test.uiautomator.UiDevice;
import androidx.annotation.VisibleForTesting;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.runner.Description;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
import android.os.SystemClock;
import android.util.Log;
@@ -30,6 +37,39 @@
@VisibleForTesting static final String KEY_DROP_CACHE = "drop-cache";
private static boolean mDropCache = true;
+ /**
+ * Shell equivalent of $(echo 3 > /proc/sys/vm/drop_caches)
+ *
+ * Clears out the system pagecache for files and inodes metadata.
+ */
+ public static void executeDropCaches() {
+ // Create a temporary file which contains the dropCaches command.
+ // Do this because we cannot write to /proc/sys/vm/drop_caches directly,
+ // as executeShellCommand parses the '>' character as a literal.
+ try {
+ File outputDir =
+ InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
+ File outputFile = File.createTempFile("drop_cache_script", "sh", outputDir);
+ outputFile.setWritable(true);
+ outputFile.setExecutable(true, /*ownersOnly*/false);
+
+ String outputFilePath = outputFile.toString();
+
+ // If this works correctly, the next log-line will print 'Success'.
+ String str = "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure";
+ BufferedWriter writer = new BufferedWriter(new FileWriter(outputFilePath));
+ writer.write(str);
+ writer.close();
+
+ UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ String result = device.executeShellCommand(outputFilePath);
+ Log.v(LOG_TAG, "dropCaches output was: " + result);
+ outputFile.delete();
+ } catch (IOException e) {
+ throw new AssertionError (e);
+ }
+ }
+
@Override
protected void starting(Description description) {
// Identify the filter option to use.
@@ -38,7 +78,7 @@
return;
}
- executeShellCommand("echo 3 > /proc/sys/vm/drop_caches");
+ executeDropCaches();
// TODO: b/117868612 to identify the root cause for additional wait.
SystemClock.sleep(3000);
}
diff --git a/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
new file mode 100644
index 0000000..87a7289
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
@@ -0,0 +1,289 @@
+/*
+ * 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 android.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.test.rule.DropCachesRule;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.InitializationError;
+
+/** This rule toggles iorap compilation for an app, or skips if unspecified. */
+public class IorapCompilationRule extends TestWatcher {
+ //
+ private static final String TAG = IorapCompilationRule.class.getSimpleName();
+ // constants
+ @VisibleForTesting static final String ARGUMENT_IORAPD_ENABLED = "iorapd-enabled";
+
+ @VisibleForTesting
+ static final String IORAP_COMPILE_CMD = "dumpsys iorapd --compile-package %s";
+ @VisibleForTesting
+ static final String IORAP_MAINTENANCE_CMD = "dumpsys iorapd --purge-package %s";
+ @VisibleForTesting
+ static final String IORAP_DUMPSYS_CMD = "dumpsys iorapd";
+
+ private static final int IORAP_COMPILE_CMD_TIMEOUT_SEC = 60; // in seconds: 1 minutes
+ private static final int IORAP_COMPILE_MIN_TRACES = 1; // configure iorapd to need 1 trace.
+ private static final int IORAP_COMPILE_RETRIES = 3; // retry compiler 3 times if it fails.
+ private static final int IORAP_TRACE_DURATION_TIMEOUT = 7000; // Allow 7s for trace to complete.
+ private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 3; // min 3 launches to merge traces.
+
+ // Global static counter. Each junit instrument command must launch 1 package with
+ // 1 compiler filter.
+ private static int sIterationCounter = 0;
+ private String mApplication;
+
+ private enum IorapStatus {
+ UNDEFINED,
+ ENABLED,
+ DISABLED
+ }
+ private static IorapStatus sIorapStatus = IorapStatus.UNDEFINED;
+
+ private enum IorapCompilationStatus {
+ INCOMPLETE,
+ COMPLETE,
+ INSUFFICIENT_TRACES,
+ }
+
+ @VisibleForTesting
+ protected static void resetState() {
+ sIorapStatus = IorapStatus.UNDEFINED;
+ sIterationCounter = 0;
+ Log.v(TAG, "resetState");
+ }
+
+ public IorapCompilationRule() throws InitializationError {
+ throw new InitializationError("Must supply an application to enable iorapd for.");
+ }
+
+ public IorapCompilationRule(String application) {
+ mApplication = application;
+ }
+
+ protected void sleep(int ms) {
+ SystemClock.sleep(ms);
+ }
+
+ // [[ $(adb shell whoami) == "root" ]]
+ protected boolean checkIfRoot() {
+ String result = executeShellCommand("whoami");
+ return result.contains("root");
+ }
+
+ // Delete all db rows and files associated with a package in iorapd.
+ // Effectively deletes any raw or compiled trace files, unoptimizing the package in iorap.
+ private void purgeIorapPackage(String packageName) {
+ if (!checkIfRoot()) {
+ throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+ }
+
+ Log.v(TAG, "Purge iorap package: " + packageName);
+ executeShellCommand(String.format(IORAP_MAINTENANCE_CMD, packageName));
+ Log.v(TAG, "Executed: " + String.format(IORAP_MAINTENANCE_CMD, packageName));
+ }
+
+ /**
+ * Toggle iorapd-based readahead and trace-collection.
+ * If iorapd is already enabled and enable is true, does nothing.
+ * If iorapd is already disabled and enable is false, does nothing.
+ */
+ private void toggleIorapStatus(boolean enable) {
+ boolean currentlyEnabled = false;
+ Log.v(TAG, "toggleIorapStatus " + Boolean.toString(enable));
+
+ // Do nothing if we are already enabled or disabled.
+ if (sIorapStatus == IorapStatus.ENABLED && enable) {
+ return;
+ } else if (sIorapStatus == IorapStatus.DISABLED && !enable) {
+ return;
+ }
+
+ if (!checkIfRoot()) {
+ throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+ }
+
+ executeShellCommand(String.format("setprop iorapd.perfetto.enable %b", enable));
+ executeShellCommand(String.format("setprop iorapd.readahead.enable %b", enable));
+ executeShellCommand(String.format(
+ "setprop iorapd.maintenance.min_traces %d", IORAP_COMPILE_MIN_TRACES));
+ executeShellCommand(String.format("dumpsys iorapd --refresh-properties"));
+
+ if (enable) {
+ sIorapStatus = IorapStatus.ENABLED;
+ } else {
+ sIorapStatus = IorapStatus.DISABLED;
+ }
+ }
+
+ /**
+ * Compile the app package using compilerFilter,
+ * retrying if the compilation command fails in between.
+ */
+ private void compileAppForIorapWithRetries(String appPkgName, int retries) {
+ for (int i = 0; i < retries; ++i) {
+ if (compileAppForIorap(appPkgName)) {
+ return;
+ }
+ sleep(1000);
+ }
+
+ throw new IllegalStateException("compileAppForIorapWithRetries: timed out after "
+ + retries + " retries");
+ }
+
+ /**
+ * Compile the app package using iorap.cmd.maintenance and return false
+ * if the compilation failed for some reason.
+ */
+ private boolean compileAppForIorap(String appPkgName) {
+ executeShellCommand(String.format(IORAP_COMPILE_CMD, appPkgName));
+
+ int i = 0;
+ for (i = 0; i < IORAP_COMPILE_CMD_TIMEOUT_SEC; ++i) {
+ IorapCompilationStatus status = waitForIorapCompiled(appPkgName);
+ if (status == IorapCompilationStatus.COMPLETE) {
+ Log.v(TAG, "compileAppForIorap: success");
+ break;
+ } else if (status == IorapCompilationStatus.INSUFFICIENT_TRACES) {
+ Log.e(TAG, "compileAppForIorap: failed due to insufficient traces");
+ throw new IllegalStateException(
+ "compileAppForIorap: failed due to insufficient traces");
+ } // else INCOMPLETE. keep asking iorapd if it's done yet.
+ sleep(1000);
+ }
+
+ if (i == IORAP_COMPILE_CMD_TIMEOUT_SEC) {
+ Log.e(TAG, "compileAppForIorap: failed due to timeout");
+ return false;
+ }
+
+ return true;
+ }
+
+ private IorapCompilationStatus waitForIorapCompiled(String appPkgName) {
+ String output = executeShellCommand(IORAP_DUMPSYS_CMD);
+
+ String prevLine = "";
+ for (String line : output.split("\n")) {
+ // Match the indented VersionedComponentName string.
+ // " com.google.android.deskclock/com.android.deskclock.DeskClock@62000712"
+ // Note: spaces are meaningful here.
+ if (prevLine.contains(" " + appPkgName) && prevLine.contains("@")) {
+ // pre-requisite:
+ // Compiled Status: Raw traces pending compilation (3)
+ if (line.contains("Compiled Status: Usable compiled trace")) {
+ return IorapCompilationStatus.COMPLETE;
+ } else if (line.contains("Compiled Status: ") &&
+ line.contains("more traces for compilation")) {
+ // Compiled Status: Need 1 more traces for compilation
+ // No amount of waiting will help here because there were
+ // insufficient traces made.
+ return IorapCompilationStatus.INSUFFICIENT_TRACES;
+ }
+ }
+ prevLine = line;
+ }
+ return IorapCompilationStatus.INCOMPLETE;
+ }
+
+ /**
+ * The first {@code IORAP_TRIAL_LAUNCH_ITERATIONS} are used for collecting an iorap trace file.
+ */
+ private boolean isIorapTraceBeingCollected() {
+ return sIterationCounter < IORAP_TRIAL_LAUNCH_ITERATIONS;
+ }
+
+ private boolean isLastIorapTraceCollection() {
+ return sIterationCounter == IORAP_TRIAL_LAUNCH_ITERATIONS - 1;
+ }
+
+ private boolean isFirstIorapTraceCollection() {
+ return sIterationCounter == 0;
+ }
+
+ /**
+ * Returns null if iorapd-enabled is unset, otherwise returns
+ * the true/false value of iorapd-enabled.
+ */
+ private Boolean isIorapdEnabled() {
+ String value = getArguments().getString(ARGUMENT_IORAPD_ENABLED);
+ if (value == null) {
+ return null;
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ @Override
+ protected void starting(Description description) {
+ // Don't do anything if iorapd-enabled was not set.
+ Boolean enabled = isIorapdEnabled();
+ if (enabled == null) {
+ Log.d(TAG, "Skipping iorapd toggling because 'iorapd-enabled' option is unset.");
+ return;
+ }
+ logStatus("starting");
+ // Compile each application in sequence.
+ String app = mApplication;
+
+ toggleIorapStatus(enabled);
+
+ if (!enabled) {
+ return;
+ }
+
+ if (isIorapTraceBeingCollected()) {
+ // Purge all iorap traces prior to first run of an application.
+ if (isFirstIorapTraceCollection()) {
+ purgeIorapPackage(mApplication);
+ }
+
+ // We must always drop caches to simulate a cold start if the app
+ // launch is going to be used for an iorap-trace collection.
+ DropCachesRule.executeDropCaches();
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ logStatus("finishing");
+
+ Boolean enabled = isIorapdEnabled();
+
+ if (Boolean.TRUE.equals(enabled) && isIorapTraceBeingCollected()) {
+ // wait for slightly more than 5s (iorapd.perfetto.trace_duration_ms) for the
+ // trace buffers to complete.
+ sleep(IORAP_TRACE_DURATION_TIMEOUT);
+
+ if (isLastIorapTraceCollection()) {
+ // run the iorap compiler and wait for iorap to compile fully.
+ // this throws an exception if it fails.
+ compileAppForIorapWithRetries(mApplication, IORAP_COMPILE_RETRIES);
+ }
+ }
+
+ logStatus("finished");
+ sIterationCounter++;
+ }
+
+ private void logStatus(String status) {
+ Log.v(TAG, String.format("%s iteration %s for app %s", status, sIterationCounter, mApplication));
+ }
+}
+
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
new file mode 100644
index 0000000..bffd026
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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 android.platform.test.rule;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit test the logic for {@link IorapCompilationRule}
+ */
+@RunWith(JUnit4.class)
+public class IorapCompilationRuleTest {
+ /**
+ * Tests that this rule will fail to register if no apps are supplied.
+ */
+ @Test
+ public void testNoAppToKillFails() {
+ try {
+ IorapCompilationRule rule = new IorapCompilationRule();
+ fail("An initialization error should have been thrown, but wasn't.");
+ } catch (InitializationError e) {
+ return;
+ }
+ }
+
+ /**
+ * Tests that this rule does nothing when 'iorapd-enabled' is unset.
+ */
+ @Test
+ public void testDoingNothingWhenParamsUnset() throws Throwable {
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(new Bundle(), "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will disable iorapd when 'iorapd-enabled' is false.
+ */
+ @Test
+ public void testDisablingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "false");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "setprop iorapd.perfetto.enable false",
+ "setprop iorapd.readahead.enable false",
+ "setprop iorapd.maintenance.min_traces 1",
+ "dumpsys iorapd --refresh-properties",
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+ */
+ @Test
+ public void testEnablingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "setprop iorapd.perfetto.enable true",
+ "setprop iorapd.readahead.enable true",
+ "setprop iorapd.maintenance.min_traces 1",
+ "dumpsys iorapd --refresh-properties",
+ String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+ */
+ @Test
+ public void testCompilingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ // The first iteration turns on iorapd and will trace the app.
+ assertThat(rule.getOperations()).containsExactly(
+ "setprop iorapd.perfetto.enable true",
+ "setprop iorapd.readahead.enable true",
+ "setprop iorapd.maintenance.min_traces 1",
+ "dumpsys iorapd --refresh-properties",
+ String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+ "test")
+ .inOrder();
+
+ // We do nothing special for the second iteration, iorapd will be tracing.
+ TestableIorapCompilationRule rule2 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule2.apply(rule2.getTestStatement(), Description.createTestDescription("clzz", "mthd2"))
+ .evaluate();
+ assertThat(rule2.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+
+ // On the 3rd iteration, we iorap compile the package after the test method finishes.
+ TestableIorapCompilationRule rule3 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule3.apply(rule3.getTestStatement(), Description.createTestDescription("clzz", "mthd3"))
+ .evaluate();
+ assertThat(rule3.getOperations()).containsExactly(
+ "test",
+ String.format(IorapCompilationRule.IORAP_COMPILE_CMD, "example.package.name"),
+ IorapCompilationRule.IORAP_DUMPSYS_CMD)
+ .inOrder();
+
+ // On the 4th and later iteration, we do nothing.
+ TestableIorapCompilationRule rule4 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule4.apply(rule4.getTestStatement(), Description.createTestDescription("clzz", "mthd4"))
+ .evaluate();
+ assertThat(rule4.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+ }
+
+ @Before
+ public void resetRuleState() {
+ TestableIorapCompilationRule.resetState();
+ }
+
+ private static class TestableIorapCompilationRule extends IorapCompilationRule {
+ private List<String> mOperations = new ArrayList<>();
+ private Bundle mBundle;
+
+ public TestableIorapCompilationRule(Bundle bundle, String app) {
+ super(app);
+ mBundle = bundle;
+ }
+
+ @Override
+ protected String executeShellCommand(String cmd) {
+ mOperations.add(cmd);
+
+ if (cmd.equals(IorapCompilationRule.IORAP_DUMPSYS_CMD)) {
+ return " example.package.name/com.android.example.Activity@62000712" +
+ "\n Compiled Status: Usable compiled trace";
+ }
+
+ return "";
+ }
+
+ @Override
+ protected Bundle getArguments() {
+ return mBundle;
+ }
+
+ public List<String> getOperations() {
+ return mOperations;
+ }
+
+ public Statement getTestStatement() {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ mOperations.add("test");
+ }
+ };
+ }
+
+ public static void resetState() {
+ IorapCompilationRule.resetState();
+ }
+
+ @Override
+ protected boolean checkIfRoot() {
+ return true;
+ }
+
+ @Override
+ protected void sleep(int ms) {
+ // Intentionally left empty. The tests don't need to sleep.
+ }
+ }
+}
+
diff --git a/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb b/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb
new file mode 100644
index 0000000..4a4ade4
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb
@@ -0,0 +1,17 @@
+schedule: INDEXED
+scenarios [{
+ index: 1
+ journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+ index: 2
+ journey: "android.platform.test.longevity.samples.SimpleSuite$FailingTest"
+}, {
+ index: 3
+ journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+ index: 4
+ journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+ index: 5
+ journey: "android.platform.test.longevity.samples.SimpleSuite$FailingTest"
+}]
diff --git a/libraries/health/runners/longevity/platform/samples/assets/sample_profile.textpb b/libraries/health/runners/longevity/platform/samples/assets/sample_scheduled_profile.textpb
similarity index 100%
rename from libraries/health/runners/longevity/platform/samples/assets/sample_profile.textpb
rename to libraries/health/runners/longevity/platform/samples/assets/sample_scheduled_profile.textpb
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
index 4cb3933..b981276 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
@@ -79,6 +79,17 @@
}
}
+ // Comparator for sorting indexed CUJs.
+ private static class ScenarioIndexedComparator implements Comparator<Scenario> {
+ public int compare(Scenario s1, Scenario s2) {
+ if (!(s1.hasIndex() && s2.hasIndex())) {
+ throw new IllegalArgumentException(
+ "Scenarios in indexed profiles must have indexes.");
+ }
+ return Integer.compare(s1.getIndex(), s2.getIndex());
+ }
+ }
+
public Profile(Bundle args) {
super();
// Set the timestamp parser to UTC to get test timstamps as "time elapsed since zero".
@@ -105,6 +116,8 @@
throw new IllegalArgumentException(
"Cannot parse the timestamp of the first scenario.", e);
}
+ } else if (mConfiguration.getSchedule().equals(Schedule.INDEXED)) {
+ Collections.sort(mOrderedScenariosList, new ScenarioIndexedComparator());
} else {
throw new UnsupportedOperationException(
"Only scheduled profiles are currently supported.");
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
index 2baa274..d557780 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
@@ -147,6 +147,11 @@
mProfile.getCurrentScenario(),
timeout,
mProfile.hasNextScheduledScenario());
+
+ case INDEXED:
+ return getIndexedRunner(
+ (BlockJUnit4ClassRunner) runner, mProfile.getCurrentScenario());
+
default:
throw new RuntimeException(
String.format(
@@ -173,4 +178,19 @@
e);
}
}
+
+ /** Replace a runner with {@link ScenarioRunner} for features specific to indexed profiles. */
+ @VisibleForTesting
+ protected ScenarioRunner getIndexedRunner(BlockJUnit4ClassRunner runner, Scenario scenario) {
+ Class<?> testClass = runner.getTestClass().getJavaClass();
+ try {
+ return new ScenarioRunner(testClass, scenario);
+ } catch (InitializationError e) {
+ throw new RuntimeException(
+ String.format(
+ "Unable to run scenario %s with an indexed runner.",
+ runner.getDescription().getDisplayName()),
+ e);
+ }
+ }
}
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java
new file mode 100644
index 0000000..33e4056
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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.platform.test.longevity;
+
+import android.os.Bundle;
+import android.platform.test.longevity.proto.Configuration.Scenario;
+import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+/** A {@link BlockJUnit4ClassRunner} that runs a test class with profile-specified options. */
+public class ScenarioRunner extends LongevityClassRunner {
+ private final Scenario mScenario;
+ private final Bundle mArguments;
+
+ public ScenarioRunner(Class<?> klass, Scenario scenario) throws InitializationError {
+ this(klass, scenario, InstrumentationRegistry.getArguments());
+ }
+
+ @VisibleForTesting
+ ScenarioRunner(Class<?> klass, Scenario scenario, Bundle arguments) throws InitializationError {
+ super(klass, arguments);
+ mScenario = scenario;
+ mArguments = arguments;
+ }
+
+ @Override
+ protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
+ // Keep a copy of the bundle arguments for restoring later.
+ Bundle modifiedArguments = mArguments.deepCopy();
+ for (ExtraArg argPair : mScenario.getExtrasList()) {
+ if (argPair.getKey() == null || argPair.getValue() == null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Each extra arg entry in scenario must have both a key and a value,"
+ + " but scenario is %s.",
+ mScenario.toString()));
+ }
+ modifiedArguments.putString(argPair.getKey(), argPair.getValue());
+ }
+ // Swap the arguments, run the scenario, and then restore arguments.
+ InstrumentationRegistry.registerInstance(
+ InstrumentationRegistry.getInstrumentation(), modifiedArguments);
+ super.runChild(method, notifier);
+ InstrumentationRegistry.registerInstance(
+ InstrumentationRegistry.getInstrumentation(), mArguments);
+ }
+}
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
index 794b576..ffd54fe 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
@@ -47,6 +47,8 @@
/**
* A {@link BlockJUnit4ClassRunner} that runs a test class with a specified timeout and optionally
* performs an idle before teardown (staying inside the app for Android CUJs).
+ *
+ * <p>TODO(b/146215435): Refactor to extends the index-based {@link ScenarioRunner}.
*/
public class ScheduledScenarioRunner extends LongevityClassRunner {
// A leeway to ensure that the teardown steps in @After and @AfterClass has time to finish.
@@ -55,7 +57,10 @@
// Please note that in most cases (when the CUJ does not time out) the actual cushion for
// teardown is double the value below, as a cushion needs to be created inside the timeout
// rule and also outside of it.
- @VisibleForTesting static final long TEARDOWN_LEEWAY_MS = 3000;
+ // This parameter is configurable via the command line as the teardown time varies across CUJs.
+ @VisibleForTesting static final String TEARDOWN_LEEWAY_OPTION = "teardown-window_ms";
+ @VisibleForTesting static final long TEARDOWN_LEEWAY_DEFAULT = 3000L;
+ private long mTeardownLeewayMs = TEARDOWN_LEEWAY_DEFAULT;
private static final String LOG_TAG = ScheduledScenarioRunner.class.getSimpleName();
@@ -84,9 +89,13 @@
mTotalTimeoutMs = max(timeout, 0);
// Ensure that the enforced timeout is non-negative. This cushion is built in so that the
// CUJ still has time for teardown steps when the test portion times out.
- mEnforcedTimeoutMs = max(mTotalTimeoutMs - TEARDOWN_LEEWAY_MS, 0);
+ mEnforcedTimeoutMs = max(mTotalTimeoutMs - mTeardownLeewayMs, 0);
mShouldIdle = shouldIdle;
mArguments = arguments;
+ mTeardownLeewayMs =
+ Long.parseLong(
+ arguments.getString(
+ TEARDOWN_LEEWAY_OPTION, String.valueOf(mTeardownLeewayMs)));
}
@Override
@@ -107,9 +116,9 @@
// Run the underlying test and report exceptions.
statement.evaluate();
} finally {
- // If there is time left for idling (i.e. more than TEARDOWN_LEEWAY_MS),
+ // If there is time left for idling (i.e. more than mTeardownLeewayMs),
// and the scenario is set to stay in app, idle for the remainder of
- // its timeout window until TEARDOWN_LEEWAY_MS before the start time of
+ // its timeout window until mTeardownLeewayMs before the start time of
// the next scenario, before executing the scenario's @After methods.
// The above does not apply if current scenario is the last one, in
// which case the idle is never performed regardless of its after_test
@@ -123,7 +132,7 @@
performIdleBeforeTeardown(
max(
getTimeRemainingForTimeoutRule()
- - TEARDOWN_LEEWAY_MS,
+ - mTeardownLeewayMs,
0));
}
}
@@ -252,4 +261,10 @@
context.unregisterReceiver(receiver);
}
}
+
+ /** Expose the teardown leeway since tests rely on it for verifying timing. */
+ @VisibleForTesting
+ long getTeardownLeeway() {
+ return mTeardownLeewayMs;
+ }
}
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
index 37bed59..d05fd5a 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
@@ -21,17 +21,17 @@
message Configuration {
// Schedule used to run the profile.
- // TODO(b/122323704): Implement ordered profile.
enum Schedule {
TIMESTAMPED = 1;
+ INDEXED = 2;
}
optional Schedule schedule = 1 [default = TIMESTAMPED];
// Information for each scenario.
message Scenario {
oneof schedule {
- // Timestamp to run the scenario in HH:MM:SS.
- string at = 1;
+ string at = 1; // A timestamp (HH:MM:SS) for when to run the scenario.
+ int32 index = 2; // An index for the relative order of the scenario.
}
// Reference to the CUJ (<package>.<class>).
optional string journey = 3;
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb b/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb
new file mode 100644
index 0000000..6f67f1e
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb
@@ -0,0 +1,11 @@
+schedule: INDEXED
+scenarios [{
+ index: 1
+ journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest1"
+}, {
+ index: 2
+ journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest2"
+}, {
+ index: 3
+ journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest1"
+}]
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
similarity index 78%
rename from libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb
rename to libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
index 4c02f42..f68cc2f 100644
--- a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb
+++ b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
@@ -1,10 +1,10 @@
schedule: TIMESTAMPED
scenarios [{
at: "00:00:01"
- journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest"
+ journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$LongIdleTest"
after_test: STAY_IN_APP
}, {
at: "00:00:10"
- journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest"
+ journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$PassingTest"
after_test: STAY_IN_APP
}]
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
similarity index 77%
rename from libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb
rename to libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
index 7c32fa7..06af14f 100644
--- a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb
+++ b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
@@ -1,9 +1,9 @@
schedule: TIMESTAMPED
scenarios [{
at: "00:00:01"
- journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest"
+ journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$PassingTest"
}, {
at: "00:00:05"
- journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest"
+ journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$LongIdleTest"
after_test: STAY_IN_APP
}]
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
index d42ccd7..8d7237d 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
@@ -30,7 +30,8 @@
import android.host.test.longevity.listener.TimeoutTerminator;
import android.os.Bundle;
import android.os.SystemClock;
-import android.platform.test.longevity.samples.testing.SampleProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleBasicProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleTimedProfileSuite;
import android.platform.test.scenario.annotation.Scenario;
import org.junit.Assert;
@@ -143,19 +144,21 @@
@RunWith(Parameterized.class)
public static class NotSupportedRunner extends BasicScenario {}
- /** Test that a profile's scheduling is followed. */
+ /** Test that a timestamped profile's scheduling is followed. */
@Test
- public void testScheduling_respectsSchedule() throws InitializationError {
+ public void testTimestampScheduling_respectsSchedule() throws InitializationError {
// TODO(harrytczhang@): Find a way to run this without relying on actual idles.
// Arguments with the profile under test.
Bundle args = new Bundle();
- args.putString(Profile.PROFILE_OPTION_NAME, "testScheduling_respectsSchedule");
+ args.putString(Profile.PROFILE_OPTION_NAME, "testTimestampScheduling_respectsSchedule");
// Scenario names from the profile.
final String firstScenarioName =
- "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest";
+ "android.platform.test.longevity.samples.testing."
+ + "SampleTimedProfileSuite$LongIdleTest";
final String secondScenarioName =
- "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest";
+ "android.platform.test.longevity.samples.testing."
+ + "SampleTimedProfileSuite$PassingTest";
// Stores the start time of the test run for the suite. Using AtomicLong here as the time
// should be initialized when run() is called on the suite, but Java does not want
// assignment to local varaible in lambda expressions. AtomicLong allows for using the
@@ -164,7 +167,7 @@
ProfileSuite suite =
spy(
new ProfileSuite(
- SampleProfileSuite.class,
+ SampleTimedProfileSuite.class,
new AllDefaultPossibilitiesBuilder(true),
mInstrumentation,
mContext,
@@ -242,22 +245,22 @@
argThat(notifier -> notifier.equals(mRunNotifier)));
}
- /** Test that a profile's last scenario is bounded by the suite timeout. */
+ /** Test that a timestamp profile's last scenario is bounded by the suite timeout. */
@Test
- public void testScheduling_respectsSuiteTimeout() throws InitializationError {
+ public void testTimestampScheduling_respectsSuiteTimeout() throws InitializationError {
long suiteTimeoutMsecs = TimeUnit.SECONDS.toMillis(10);
ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
// Arguments with the profile under test and suite timeout.
Bundle args = new Bundle();
- args.putString(Profile.PROFILE_OPTION_NAME, "testScheduling_respectsSuiteTimeout");
+ args.putString(Profile.PROFILE_OPTION_NAME, "testTimestampScheduling_respectsSuiteTimeout");
args.putString(TimeoutTerminator.OPTION, String.valueOf(suiteTimeoutMsecs));
// Construct and run the profile suite.
ProfileSuite suite =
spy(
new ProfileSuite(
- SampleProfileSuite.class,
+ SampleTimedProfileSuite.class,
new AllDefaultPossibilitiesBuilder(true),
mInstrumentation,
mContext,
@@ -288,10 +291,71 @@
long expectedTimeout =
suiteTimeoutMsecs
- TimeUnit.SECONDS.toMillis(4)
- - ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS;
+ - ScheduledScenarioRunner
+ .TEARDOWN_LEEWAY_DEFAULT;
return abs(exceptionTimeout - expectedTimeout)
<= SCHEDULE_LEEWAY_MS;
});
Assert.assertTrue(correctTestTimedOutExceptionFired);
}
+
+ /** Test that an indexed profile's scheduling is followed. */
+ @Test
+ public void testIndexedScheduling_respectsSchedule() throws InitializationError {
+ // Arguments with the profile under test.
+ Bundle args = new Bundle();
+ args.putString(Profile.PROFILE_OPTION_NAME, "testIndexedScheduling_respectsSchedule");
+ // Scenario names from the profile.
+ final String firstScenarioName =
+ "android.platform.test.longevity.samples.testing."
+ + "SampleBasicProfileSuite$PassingTest1";
+ final String secondScenarioName =
+ "android.platform.test.longevity.samples.testing."
+ + "SampleBasicProfileSuite$PassingTest2";
+ final String thirdScenarioName =
+ "android.platform.test.longevity.samples.testing."
+ + "SampleBasicProfileSuite$PassingTest1";
+ ProfileSuite suite =
+ spy(
+ new ProfileSuite(
+ SampleBasicProfileSuite.class,
+ new AllDefaultPossibilitiesBuilder(true),
+ mInstrumentation,
+ mContext,
+ args));
+
+ InOrder inOrderVerifier = inOrder(suite);
+
+ suite.run(mRunNotifier);
+ // Verify that the first scenario is started.
+ inOrderVerifier
+ .verify(suite)
+ .runChild(
+ argThat(
+ runner ->
+ runner.getDescription()
+ .getDisplayName()
+ .equals(firstScenarioName)),
+ argThat(notifier -> notifier.equals(mRunNotifier)));
+ // Verify that the second scenario is started.
+ inOrderVerifier
+ .verify(suite)
+ .runChild(
+ argThat(
+ runner ->
+ runner.getDescription()
+ .getDisplayName()
+ .equals(secondScenarioName)),
+ argThat(notifier -> notifier.equals(mRunNotifier)));
+ // Verify that the third scenario is started.
+ inOrderVerifier
+ .verify(suite)
+ .runChild(
+ argThat(
+ runner ->
+ runner.getDescription()
+ .getDisplayName()
+ .equals(thirdScenarioName)),
+ argThat(notifier -> notifier.equals(mRunNotifier)));
+ }
}
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java
new file mode 100644
index 0000000..a6f3c50
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2019 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.platform.test.longevity;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.os.Bundle;
+import android.platform.test.longevity.proto.Configuration.Scenario;
+import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.exceptions.base.MockitoAssertionError;
+
+import java.util.HashSet;
+import java.util.List;
+
+/** Unit tests for the {@link ScenarioRunner} runner. */
+@RunWith(JUnit4.class)
+public class ScenarioRunnerTest {
+
+ @Mock private RunNotifier mRunNotifier;
+
+ private static final String ASSERTION_FAILURE_MESSAGE = "Test assertion failed";
+
+ public static class ArgumentTest {
+ public static final String TEST_ARG = "test-arg-test-only";
+ public static final String TEST_ARG_DEFAULT = "default";
+ public static final String TEST_ARG_OVERRIDE = "not default";
+
+ @Before
+ public void setUp() {
+ // The actual argument testing happens here as this is where instrumentation args are
+ // parsed in the CUJs.
+ String argValue =
+ InstrumentationRegistry.getArguments().getString(TEST_ARG, TEST_ARG_DEFAULT);
+ Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, argValue, TEST_ARG_OVERRIDE);
+ }
+
+ @Test
+ public void dummyTest() {
+ // Does nothing; always passes.
+ }
+ }
+
+ // Holds the state of the instrumentation args before each test for restoring after, as one test
+ // might affect the state of another otherwise.
+ // TODO(b/124239142): Avoid manipulating the instrumentation args here.
+ private Bundle mArgumentsBeforeTest;
+
+ @Before
+ public void setUpSuite() throws InitializationError {
+ initMocks(this);
+ mArgumentsBeforeTest = InstrumentationRegistry.getArguments();
+ }
+
+ @After
+ public void restoreSuite() {
+ InstrumentationRegistry.registerInstance(
+ InstrumentationRegistry.getInstrumentation(), mArgumentsBeforeTest);
+ }
+
+ /** Test that the "extras" in a scenario is properly registered before the test. */
+ @Test
+ public void testExtraArgs_registeredBeforeTest() throws Throwable {
+ Scenario testScenario =
+ Scenario.newBuilder()
+ .setIndex(1)
+ .setJourney(ArgumentTest.class.getName())
+ .addExtras(
+ ExtraArg.newBuilder()
+ .setKey(ArgumentTest.TEST_ARG)
+ .setValue(ArgumentTest.TEST_ARG_OVERRIDE))
+ .build();
+ ScenarioRunner runner = spy(new ScenarioRunner(ArgumentTest.class, testScenario));
+ runner.run(mRunNotifier);
+ verifyForAssertionFailures(mRunNotifier);
+ }
+
+ /** Test that the "extras" in a scenario is properly un-registered after the test. */
+ @Test
+ public void testExtraArgs_unregisteredAfterTest() throws Throwable {
+ Bundle argsBeforeTest = InstrumentationRegistry.getArguments();
+ Scenario testScenario =
+ Scenario.newBuilder()
+ .setIndex(1)
+ .setJourney(ArgumentTest.class.getName())
+ .addExtras(
+ ExtraArg.newBuilder()
+ .setKey(ArgumentTest.TEST_ARG)
+ .setValue(ArgumentTest.TEST_ARG_OVERRIDE))
+ .build();
+ ScenarioRunner runner = spy(new ScenarioRunner(ArgumentTest.class, testScenario));
+ runner.run(mRunNotifier);
+ Bundle argsAfterTest = InstrumentationRegistry.getArguments();
+ Assert.assertTrue(bundlesContainSameStringKeyValuePairs(argsBeforeTest, argsAfterTest));
+ }
+
+ /**
+ * Verify that no test failure is fired because of an assertion failure in the stubbed methods.
+ * If the verification fails, check whether it's due the injected assertions failing. If yes,
+ * throw that exception out; otherwise, throw the first exception.
+ */
+ private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
+ try {
+ verify(notifier, never()).fireTestFailure(any());
+ } catch (MockitoAssertionError e) {
+ ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
+ verify(notifier, atLeastOnce()).fireTestFailure(failureCaptor.capture());
+ List<Failure> failures = failureCaptor.getAllValues();
+ // Go through the failures, look for an known failure case from the above exceptions
+ // and throw the exception in the first one out if any.
+ for (Failure failure : failures) {
+ if (failure.getException().getMessage().contains(ASSERTION_FAILURE_MESSAGE)) {
+ throw failure.getException();
+ }
+ }
+ // Otherwise, throw the exception from the first failure reported.
+ throw failures.get(0).getException();
+ }
+ }
+
+ /**
+ * Helper method to check whether two {@link Bundle}s are equal since the built-in {@code
+ * equals} is not properly overridden.
+ */
+ private boolean bundlesContainSameStringKeyValuePairs(Bundle b1, Bundle b2) {
+ if (b1.size() != b2.size()) {
+ return false;
+ }
+ HashSet<String> allKeys = new HashSet<String>(b1.keySet());
+ allKeys.addAll(b2.keySet());
+ for (String key : allKeys) {
+ if (b1.getString(key) != null) {
+ // If key is in b1 and corresponds to a string, check whether this key corresponds
+ // to the same value in b2.
+ if (!b1.getString(key).equals(b2.getString(key))) {
+ return false;
+ }
+ } else if (b2.getString(key) != null) {
+ // Otherwise if b2 has a string at this key, return false since we know that b1 does
+ // not have a string at this key.
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
index a65fce0..78bc467 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
@@ -31,7 +31,7 @@
import android.platform.test.longevity.proto.Configuration.Scenario;
import android.platform.test.longevity.proto.Configuration.Scenario.AfterTest;
import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
-import android.platform.test.longevity.samples.testing.SampleProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleTimedProfileSuite;
import androidx.test.InstrumentationRegistry;
import org.junit.Assert;
@@ -114,13 +114,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.LongIdleTest.class,
+ SampleTimedProfileSuite.LongIdleTest.class,
testScenario,
timeoutMs,
true));
@@ -141,8 +141,7 @@
exception
.getTimeUnit()
.toMillis(exception.getTimeout());
- long expectedTimeout =
- timeoutMs - ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS;
+ long expectedTimeout = timeoutMs - runner.getTeardownLeeway();
return abs(exceptionTimeout - expectedTimeout)
<= TIMING_LEEWAY_MS;
});
@@ -157,13 +156,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.LongIdleTest.class,
+ SampleTimedProfileSuite.LongIdleTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
true));
@@ -181,13 +180,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.LongIdleTest.class,
+ SampleTimedProfileSuite.LongIdleTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
true));
@@ -196,8 +195,7 @@
// the leeway set in @{link ScheduledScenarioRunner}.
verify(runner, times(1))
.performIdleBeforeNextScenario(
- getWithinMarginMatcher(
- ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS, TIMING_LEEWAY_MS));
+ getWithinMarginMatcher(runner.getTeardownLeeway(), TIMING_LEEWAY_MS));
}
/** Test that a test set to stay in the app after the test idles after its @Test method. */
@@ -209,13 +207,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.PassingTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.PassingTest.class,
+ SampleTimedProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true));
@@ -226,8 +224,7 @@
verify(runner, times(1))
.performIdleBeforeTeardown(
getWithinMarginMatcher(
- timeoutMs - 2 * ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS,
- TIMING_LEEWAY_MS));
+ timeoutMs - 2 * runner.getTeardownLeeway(), TIMING_LEEWAY_MS));
// Test should have passed.
verify(mRunNotifier, never()).fireTestFailure(any(Failure.class));
}
@@ -241,13 +238,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.PassingTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.EXIT)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.PassingTest.class,
+ SampleTimedProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true));
@@ -268,17 +265,17 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.PassingTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.EXIT)
.build();
Bundle ignores = new Bundle();
ignores.putString(
LongevityClassRunner.FILTER_OPTION,
- SampleProfileSuite.PassingTest.class.getCanonicalName());
+ SampleTimedProfileSuite.PassingTest.class.getCanonicalName());
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.PassingTest.class,
+ SampleTimedProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true,
@@ -304,13 +301,13 @@
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
- .setJourney(SampleProfileSuite.PassingTest.class.getName())
+ .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
- SampleProfileSuite.PassingTest.class,
+ SampleTimedProfileSuite.PassingTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
false));
@@ -389,6 +386,23 @@
Assert.assertTrue(abs(actualSleepDuration - expectedSleepMillis) <= TIMING_LEEWAY_MS);
}
+ /** Test that the teardown leeway override works. */
+ @Test
+ public void testTeardownLeewayOverride() throws Throwable {
+ Bundle args = new Bundle();
+ long leewayOverride = 1000L;
+ args.putString(
+ ScheduledScenarioRunner.TEARDOWN_LEEWAY_OPTION, String.valueOf(leewayOverride));
+ ScheduledScenarioRunner runner =
+ new ScheduledScenarioRunner(
+ ArgumentTest.class,
+ Scenario.newBuilder().build(),
+ TimeUnit.SECONDS.toMillis(6),
+ false,
+ args);
+ Assert.assertEquals(leewayOverride, runner.getTeardownLeeway());
+ }
+
/**
* Helper method to get an argument matcher that checks whether the input value is equal to
* expected value within a margin.
@@ -399,7 +413,7 @@
/**
* Verify that no test failure is fired because of an assertion failure in the stubbed methods.
- * If the verfication fails, check whether it's due the injected assertions failing. If yes,
+ * If the verification fails, check whether it's due the injected assertions failing. If yes,
* throw that exception out; otherwise, throw the first exception.
*/
private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
@@ -423,7 +437,7 @@
/**
* Helper method to check whether two {@link Bundle}s are equal since the built-in {@code
- * equals} is not properly overriden.
+ * equals} is not properly overridden.
*/
private boolean bundlesContainSameStringKeyValuePairs(Bundle b1, Bundle b2) {
if (b1.size() != b2.size()) {
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
similarity index 64%
copy from libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
copy to libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
index 2b1bf10..2c0cd5f 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
@@ -16,53 +16,38 @@
package android.platform.test.longevity.samples.testing;
-import android.os.SystemClock;
import android.platform.test.longevity.ProfileSuite;
import android.platform.test.scenario.annotation.Scenario;
-import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.junit.runners.Suite.SuiteClasses;
-import java.util.concurrent.TimeUnit;
-
@RunWith(ProfileSuite.class)
@SuiteClasses({
- SampleProfileSuite.LongIdleTest.class,
- SampleProfileSuite.PassingTest.class,
+ SampleBasicProfileSuite.PassingTest1.class,
+ SampleBasicProfileSuite.PassingTest2.class,
})
/** Sample device-side test cases using a profile. */
-public class SampleProfileSuite {
+public class SampleBasicProfileSuite {
+
@Scenario
@RunWith(JUnit4.class)
- public static class LongIdleTest {
+ public static class PassingTest1 {
@Test
- public void testLongIdle() {
- SystemClock.sleep(TimeUnit.SECONDS.toMillis(10));
- }
-
- // Simulates a quick teardown step.
- @AfterClass
- public static void dummyTearDown() {
- SystemClock.sleep(100);
+ public void testPassing() {
+ Assert.assertEquals(1, 1);
}
}
@Scenario
@RunWith(JUnit4.class)
- public static class PassingTest {
+ public static class PassingTest2 {
@Test
public void testPassing() {
- Assert.assertEquals(1, 1);
- }
-
- // Simulates a quick teardown step.
- @AfterClass
- public static void dummyTearDown() {
- SystemClock.sleep(100);
+ Assert.assertEquals(2, 2);
}
}
}
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
similarity index 93%
rename from libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
rename to libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
index 2b1bf10..16686ee 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
@@ -31,11 +31,11 @@
@RunWith(ProfileSuite.class)
@SuiteClasses({
- SampleProfileSuite.LongIdleTest.class,
- SampleProfileSuite.PassingTest.class,
+ SampleTimedProfileSuite.LongIdleTest.class,
+ SampleTimedProfileSuite.PassingTest.class,
})
/** Sample device-side test cases using a profile. */
-public class SampleProfileSuite {
+public class SampleTimedProfileSuite {
@Scenario
@RunWith(JUnit4.class)
public static class LongIdleTest {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
index dcbe7e1..c547f0d 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
@@ -338,11 +338,11 @@
mDevice.waitForIdle();
}
- UiObject2 app = mDevice.findObject(appSelector);
+ UiObject2 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
while (app == null && down.isEnabled()) {
down.click();
mDevice.waitForIdle();
- app = mDevice.findObject(appSelector);
+ app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
}
return app;
} else {
diff --git a/scripts/perfetto-setup/Android.mk b/scripts/perfetto-setup/Android.mk
index 180ddb2..ab478ec 100644
--- a/scripts/perfetto-setup/Android.mk
+++ b/scripts/perfetto-setup/Android.mk
@@ -31,6 +31,22 @@
include $(BUILD_PREBUILT)
include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config.textproto
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := prebuilts/tools/linux-x86_64/perfetto/configs/trace_config.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config_experimental.textproto
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := prebuilts/tools/linux-x86_64/perfetto/configs/trace_config_experimental.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
LOCAL_MODULE := perfetto_trace_processor_shell
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_TAGS := optional
diff --git a/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java b/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
index 1f5e234..a2e1290 100644
--- a/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
+++ b/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
@@ -57,8 +57,11 @@
@After
public void tearDown() throws RemoteException {
if (mTurnScreenBackOn.get()) {
- mDevice.wakeUp();
+ // Wake up the display. wakeUp() is not used here as when the duration is short, the
+ // device might register a double power button press and launch camera.
+ mDevice.pressMenu();
mDevice.waitForIdle();
+ // Unlock the screen.
mDevice.pressMenu();
mDevice.waitForIdle();
}
diff --git a/tests/health/scenarios/tests/Android.bp b/tests/health/scenarios/tests/Android.bp
index 9ac9a37..076a26c 100644
--- a/tests/health/scenarios/tests/Android.bp
+++ b/tests/health/scenarios/tests/Android.bp
@@ -59,6 +59,8 @@
name: "PlatformCommonScenarioTests",
min_sdk_version: "24",
static_libs: [
+ "android-support-test",
+ "collector-device-lib-platform",
"common-platform-scenarios",
"common-profile-text-protos",
"guava",
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
index 4b15e88..03b9da6 100644
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
@@ -18,6 +18,7 @@
import android.platform.test.microbenchmark.Microbenchmark;
import android.platform.test.rule.CompilationFilterRule;
import android.platform.test.rule.DropCachesRule;
+import android.platform.test.rule.IorapCompilationRule;
import android.platform.test.rule.KillAppsRule;
import android.platform.test.rule.PressHomeRule;
@@ -33,5 +34,6 @@
RuleChain.outerRule(new KillAppsRule(sPkgOption.get()))
.around(new DropCachesRule())
.around(new CompilationFilterRule(sPkgOption.get()))
- .around(new PressHomeRule());
+ .around(new PressHomeRule())
+ .around(new IorapCompilationRule(sPkgOption.get()));
}
diff --git a/tests/jank/UbSystemUiJankTests/AndroidTest.xml b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
index 3fe63fc..0923ff2 100644
--- a/tests/jank/UbSystemUiJankTests/AndroidTest.xml
+++ b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
@@ -23,7 +23,7 @@
</target_preparer>
<option name="test-tag" value="UbSystemUiJankTests" />
- <test class="com.android.tradefed.testtype.InstrumentationTest" >
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.platform.systemui.tests.jank" />
<option name="runner" value="android.test.InstrumentationTestRunner" />
<option name="hidden-api-checks" value="false" />
diff --git a/tests/jank/uibench/AndroidManifest.xml b/tests/jank/uibench/AndroidManifest.xml
index 56d3e4a..da61649 100644
--- a/tests/jank/uibench/AndroidManifest.xml
+++ b/tests/jank/uibench/AndroidManifest.xml
@@ -16,14 +16,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.uibench.janktests">
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
<application>
<uses-library android:name="android.test.runner" />
</application>
- <uses-sdk android:minSdkVersion="19"
- android:targetSdkVersion="23"/>
-
<instrumentation
android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.uibench.janktests"
diff --git a/tests/microbenchmarks/uibench/AndroidManifest.xml b/tests/microbenchmarks/uibench/AndroidManifest.xml
index 0265f2b..074f0d8 100644
--- a/tests/microbenchmarks/uibench/AndroidManifest.xml
+++ b/tests/microbenchmarks/uibench/AndroidManifest.xml
@@ -18,6 +18,7 @@
package="com.android.uibench.microbenchmark">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REAL_GET_TASKS" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner" />
diff --git a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
index 6d5136f..1df767c 100644
--- a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
@@ -121,18 +121,18 @@
}
createLaunchIntentMappings();
- String mAppsList = mArgs.getString(LAUNCH_APPS);
- mPreAppsList = mArgs.getString(PRE_LAUNCH_APPS);
+
+ String appsList = mArgs.getString(LAUNCH_APPS, "");
+ mPreAppsList = mArgs.getString(PRE_LAUNCH_APPS, "");
mLaunchIterations = Integer.parseInt(mArgs.getString(KEY_LAUNCH_ITERATIONS,
DEFAULT_LAUNCH_COUNT));
mPostLaunchTimeout = Integer.parseInt(mArgs.getString(KEY_POST_LAUNCH_TIMEOUT,
DEFAULT_POST_LAUNCH_TIMEOUT));
- if (null == mAppsList && mAppsList.isEmpty()) {
+ if (null == appsList || appsList.isEmpty()) {
throw new IllegalArgumentException("Need atleast one app to do the"
+ " app transition from launcher");
}
- mAppsList = mAppsList.replaceAll("%"," ");
- mAppListArray = mAppsList.split(DELIMITER);
+ mAppListArray = appsList.split(DELIMITER);
// Parse the trace parameters
mTraceDirectoryStr = mArgs.getString(KEY_TRACE_DIRECTORY);
@@ -266,11 +266,10 @@
if (isTracesEnabled()) {
createTraceDirectory("testAppToRecents");
}
- if (null == mPreAppsList && mPreAppsList.isEmpty()) {
+ if (null == mPreAppsList || mPreAppsList.isEmpty()) {
throw new IllegalArgumentException("Need atleast few apps in the "
+ "recents before starting the test");
}
- mPreAppsList = mPreAppsList.replaceAll("%"," ");
mPreAppsListArray = mPreAppsList.split(DELIMITER);
mPreAppsComponentName.clear();
populateRecentsList();
@@ -311,11 +310,10 @@
if (isTracesEnabled()) {
createTraceDirectory("testHotLaunchFromRecents");
}
- if (null == mPreAppsList && mPreAppsList.isEmpty()) {
+ if (null == mPreAppsList || mPreAppsList.isEmpty()) {
throw new IllegalArgumentException("Need atleast few apps in the"
+ " recents before starting the test");
}
- mPreAppsList = mPreAppsList.replaceAll("%", " ");
mPreAppsListArray = mPreAppsList.split(DELIMITER);
mPreAppsComponentName.clear();
populateRecentsList();
@@ -567,7 +565,8 @@
* @param appNames
*/
private void closeApps(String[] appNames) {
- for (int i = 0; i < appNames.length; i++) {
+ int length = appNames == null ? 0 : appNames.length;
+ for (int i = 0; i < length; i++) {
Intent startIntent = mAppLaunchIntentsMapping.get(appNames[i]);
if (startIntent != null) {
String packageName = startIntent.getComponent().getPackageName();