Add timestamp, atomic counter test and test for corner cases
android.hardware.cts.SensorDirectReportTest
* testTimestampAccel
* testTimestampGyro
* testTimestampMag
* testAtomicCounterAccel
* testAtomicCounterGyro
* testAtomicCounterMag
* testRegisterMultipleChannels
* testReconfigure
Test: SensorDirectReportTest passes on test devices
Change-Id: I4730695d684ae52ae1243aa46332cff2e35be442
Merged-In: I4730695d684ae52ae1243aa46332cff2e35be442
diff --git a/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java b/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
index 143b3a7..fd3935d 100644
--- a/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
+++ b/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
@@ -54,10 +54,22 @@
* - test<Sensor><SharedMemoryType><RateLevel>
* tests basic operation of sensor in direct report mode at various rate level specification.
* - testRateIndependency<Sensor1><Sensor2>SingleChannel
- * tests two sensors in the same direct channel are able to run at different rate.
+ * tests if two sensors in the same direct channel are able to run at different rates.
* - testRateIndependency<Sensor>MultiChannel
- * tests a sensor is able to be configured to different rate level for multiple channels.
+ * tests if a sensor is able to be configured to different rate levels for multiple channels.
* - testRateIndependency<Sensor>MultiMode
+ * tests if a sensor is able to report at different rates in direct report mode and traditional
+ * report mode (polling).
+ * - testTimestamp<Sensor>
+ * tests if the timestamp is correct both in absolute sense and relative to traditional report.
+ * - testAtomicCounter<Sensor>
+ * test if atomic counter is increased as specified and if sensor event content is fully updated
+ * before update of atomic counter.
+ * - testRegisterMultipleChannels
+ * test scenarios when multiple channels are registered simultaneously.
+ * - testReconfigure
+ * test channel reconfiguration (configure to a rate level; configure to stop; configure to
+ * another rate level)
*/
public class SensorDirectReportTest extends SensorTestCase {
private static final String TAG = "SensorDirectReportTest";
@@ -75,8 +87,8 @@
private static final float FREQ_UPPER_BOUND_POLL = 2.00f;
// sensor reading assumption
- private static final float GRAVITY_MIN = 9.81f - 0.5f;
- private static final float GRAVITY_MAX = 9.81f + 0.5f;
+ private static final float GRAVITY_MIN = 9.81f - 1.0f;
+ private static final float GRAVITY_MAX = 9.81f + 1.0f;
private static final float GYRO_NORM_MAX = 0.1f;
// test constants
@@ -369,6 +381,82 @@
runMultiModeRateIndependencyTestGroup(Sensor.TYPE_MAGNETIC_FIELD);
}
+ public void testTimestampAccel() {
+ runTimestampTestGroup(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ public void testTimestampGyro() {
+ runTimestampTestGroup(Sensor.TYPE_GYROSCOPE);
+ }
+
+ public void testTimestampMag() {
+ runTimestampTestGroup(Sensor.TYPE_MAGNETIC_FIELD);
+ }
+
+ public void testAtomicCounterAccel() {
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ runAtomicCounterTest(Sensor.TYPE_ACCELEROMETER, memType);
+ }
+ }
+
+ public void testAtomicCounterGyro() {
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ runAtomicCounterTest(Sensor.TYPE_GYROSCOPE, memType);
+ }
+ }
+
+ public void testAtomicCounterMag() {
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ runAtomicCounterTest(Sensor.TYPE_MAGNETIC_FIELD, memType);
+ }
+ }
+
+ public void testRegisterMultipleChannels() throws AssertionError {
+ resetEvent();
+ freeSharedMemory();
+
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ if (!isMemoryTypeNeeded(memType)) {
+ continue;
+ }
+
+ for (int repeat = 0; repeat < 10; ++repeat) {
+ // allocate new memory every time
+ allocateSharedMemory();
+
+ mChannel = prepareDirectChannel(memType, false /* secondary */);
+ assertNotNull("mChannel is null", mChannel);
+
+ mChannelSecondary = prepareDirectChannel(memType, true /* secondary */);
+ assertNotNull("mChannelSecondary is null", mChannelSecondary);
+
+ if (mChannel != null) {
+ mChannel.close();
+ mChannel = null;
+ }
+ if (mChannelSecondary != null) {
+ mChannelSecondary.close();
+ mChannelSecondary = null;
+ }
+
+ // free shared memory
+ freeSharedMemory();
+ }
+ }
+ }
+
+ public void testReconfigure() {
+ TestResultCollector c = new TestResultCollector("testReconfigure", TAG);
+
+ for (int type : POSSIBLE_SENSOR_TYPES) {
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ c.perform(() -> { runReconfigureTest(type, memType);},
+ String.format("sensor type %d, mem type %d", type, memType));
+ }
+ }
+ c.judge();
+ }
+
private void runSingleChannelRateIndependencyTestGroup(int type1, int type2) {
if (type1 == type2) {
throw new IllegalArgumentException("Cannot run single channel rate independency test "
@@ -441,6 +529,24 @@
c.judge();
}
+ private void runTimestampTestGroup(int sensorType) {
+ String stype = SensorCtsHelper.sensorTypeShortString(sensorType);
+
+ TestResultCollector c =
+ new TestResultCollector("testTimestamp" + stype, TAG);
+
+ for (int rateLevel : POSSIBLE_RATE_LEVELS) {
+ for (int memType : POSSIBLE_CHANNEL_TYPES) {
+ c.perform(
+ () -> {
+ runTimestampTest(sensorType, rateLevel, memType);
+ },
+ String.format("(%s, rate %d, memtype %d)", stype, rateLevel, memType));
+ }
+ }
+ c.judge();
+ }
+
private void runSensorDirectReportTest(int sensorType, int memType, int rateLevel)
throws AssertionError {
Sensor s = mSensorManager.getDefaultSensor(sensorType);
@@ -630,6 +736,137 @@
}
}
+ private void runTimestampTest(int type, int rateLevel, int memType) {
+ Sensor s = mSensorManager.getDefaultSensor(type);
+ if (s == null
+ || s.getHighestDirectReportRateLevel() < rateLevel
+ || !s.isDirectChannelTypeSupported(memType)) {
+ return;
+ }
+ resetEvent();
+
+ mChannel = prepareDirectChannel(memType, false /* secondary */);
+ assertTrue("createDirectChannel failed", mChannel != null);
+
+ SensorEventCollection listener = new SensorEventCollection(s);
+
+ try {
+ float nominalFreq = getNominalFreq(rateLevel);
+ int samplingPeriodUs = Math.max((int) (1e6f / nominalFreq), s.getMinDelay());
+
+ assertTrue("Shared memory is not formatted",
+ isSharedMemoryFormatted(memType));
+
+ int token = mChannel.configure(s, rateLevel);
+ assertTrue("configure direct mChannel failed", token > 0);
+
+ boolean registerRet = mSensorManager.registerListener(listener, s, samplingPeriodUs);
+ assertTrue("Register listener failed", registerRet);
+
+ List<DirectReportSensorEvent> events = collectSensorEventsRealtime(
+ memType, false /*secondary*/, TEST_RUN_TIME_PERIOD_MILLISEC);
+ assertTrue("Realtime event collection failed", events != null);
+ assertTrue("Realtime event collection got no data", events.size() > 0);
+
+ //stop sensor and analyze content
+ mChannel.configure(s, SensorDirectChannel.RATE_STOP);
+ mSensorManager.unregisterListener(listener);
+
+ // check rate
+ checkTimestampRelative(events, listener.getEvents());
+ checkTimestampAbsolute(events);
+ } finally {
+ mChannel.close();
+ mChannel = null;
+ }
+ }
+
+ private void runAtomicCounterTest(int sensorType, int memType) throws AssertionError {
+ Sensor s = mSensorManager.getDefaultSensor(sensorType);
+ if (s == null
+ || s.getHighestDirectReportRateLevel() == SensorDirectChannel.RATE_STOP
+ || !s.isDirectChannelTypeSupported(memType)) {
+ return;
+ }
+ resetEvent();
+
+ mChannel = prepareDirectChannel(memType, false /* secondary */);
+ assertTrue("createDirectChannel failed", mChannel != null);
+
+ try {
+ assertTrue("Shared memory is not formatted", isSharedMemoryFormatted(memType));
+ waitBeforeStartSensor();
+
+ //int token = mChannel.configure(s, SensorDirectChannel.RATE_FAST);
+ int token = mChannel.configure(s, s.getHighestDirectReportRateLevel());
+ assertTrue("configure direct mChannel failed", token > 0);
+
+ checkAtomicCounterUpdate(memType, 30 * 1000); // half min
+
+ //stop sensor and analyze content
+ mChannel.configure(s, SensorDirectChannel.RATE_STOP);
+ } finally {
+ mChannel.close();
+ mChannel = null;
+ }
+ }
+
+ private void runReconfigureTest(int type, int memType) {
+ Sensor s = mSensorManager.getDefaultSensor(type);
+ if (s == null
+ || s.getHighestDirectReportRateLevel() == SensorDirectChannel.RATE_STOP
+ || !s.isDirectChannelTypeSupported(memType)) {
+ return;
+ }
+ resetEvent();
+
+ mChannel = prepareDirectChannel(memType, false /* secondary */);
+ assertTrue("createDirectChannel failed", mChannel != null);
+
+ try {
+ assertTrue("Shared memory is not formatted", isSharedMemoryFormatted(memType));
+ waitBeforeStartSensor();
+
+ int offset = 0;
+ long counter = 1;
+ List<Integer> rateLevels = new ArrayList<>();
+ List<DirectReportSensorEvent> events;
+
+ rateLevels.add(s.getHighestDirectReportRateLevel());
+ rateLevels.add(s.getHighestDirectReportRateLevel());
+ if (s.getHighestDirectReportRateLevel() != SensorDirectChannel.RATE_NORMAL) {
+ rateLevels.add(SensorDirectChannel.RATE_NORMAL);
+ }
+
+ for (int rateLevel : rateLevels) {
+ int token = mChannel.configure(s, rateLevel);
+ assertTrue("configure direct mChannel failed", token > 0);
+
+ events = collectSensorEventsRealtime(memType, false /*secondary*/,
+ TEST_RUN_TIME_PERIOD_MILLISEC,
+ offset, counter);
+ // stop sensor
+ mChannel.configure(s, SensorDirectChannel.RATE_STOP);
+ checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, events, type, rateLevel);
+
+ // collect all events after stop
+ events = collectSensorEventsRealtime(memType, false /*secondary*/,
+ REST_PERIOD_BEFORE_TEST_MILLISEC,
+ offset, counter);
+ if (events.size() > 0) {
+ offset += (events.size() * SENSORS_EVENT_SIZE ) % SHARED_MEMORY_SIZE;
+ counter = events.get(events.size() - 1).serial;
+ }
+ }
+
+ // finally stop the report
+ mChannel.configure(s, SensorDirectChannel.RATE_STOP);
+ } finally {
+ mChannel.close();
+ mChannel = null;
+ }
+ }
+
private void waitBeforeStartSensor() {
// wait for sensor system to come to a rest after previous test to avoid flakiness.
try {
@@ -648,6 +885,147 @@
}
}
+ private List<DirectReportSensorEvent> collectSensorEventsRealtime(
+ int memType, boolean secondary, int timeoutMs) {
+ return collectSensorEventsRealtime(memType, secondary, timeoutMs,
+ 0 /*initialOffset*/, 1l /*initialCounter*/);
+ }
+
+ private List<DirectReportSensorEvent> collectSensorEventsRealtime(
+ int memType, boolean secondary, int timeoutMs, int initialOffset, long initialCounter) {
+ List<DirectReportSensorEvent> events = new ArrayList<>();
+ long endTime = SystemClock.elapsedRealtime() + timeoutMs;
+
+ long atomicCounter = initialCounter;
+ int offset = initialOffset;
+
+ long timeA = SystemClock.elapsedRealtimeNanos();
+ boolean synced = false;
+ int filtered = 0;
+
+ while (SystemClock.elapsedRealtime() < endTime) {
+ if (!readSharedMemory(
+ memType, secondary, offset + ATOMIC_COUNTER_OFFSET, ATOMIC_COUNTER_SIZE)) {
+ return null;
+ }
+
+ long timeB = SystemClock.elapsedRealtimeNanos();
+ if (timeB - timeA > 1_000_000L ) { // > 1ms
+ synced = false;
+ }
+ timeA = timeB;
+
+ if (readAtomicCounter(offset) == atomicCounter) {
+ // read entire event again and parse
+ if (!readSharedMemory(memType, secondary, offset, SENSORS_EVENT_SIZE)) {
+ return null;
+ }
+ DirectReportSensorEvent e = mEventPool.get();
+ assertNotNull("cannot get event from reserve", e);
+ parseSensorEvent(offset, e);
+
+ atomicCounter += 1;
+ if (synced) {
+ events.add(e);
+ } else {
+ ++filtered;
+ }
+
+ offset += SENSORS_EVENT_SIZE;
+ if (offset + SENSORS_EVENT_SIZE > SHARED_MEMORY_SIZE) {
+ offset = 0;
+ }
+ } else {
+ synced = true;
+ }
+ }
+ Log.d(TAG, "filtered " + filtered + " events, remain " + events.size() + " events");
+ return events;
+ }
+
+ private void checkAtomicCounterUpdate(int memType, int timeoutMs) {
+ List<DirectReportSensorEvent> events = new ArrayList<>();
+ long endTime = SystemClock.elapsedRealtime() + timeoutMs;
+
+ boolean lastValid = false;
+ long atomicCounter = 1;
+ int lastOffset = 0;
+ int offset = 0;
+
+ byte[] lastArray = new byte[SENSORS_EVENT_SIZE];
+ DirectReportSensorEvent e = getEvent();
+
+ while (SystemClock.elapsedRealtime() < endTime) {
+ if (!readSharedMemory(memType, false/*secondary*/, lastOffset, SENSORS_EVENT_SIZE)
+ || !readSharedMemory(memType, false/*secondary*/,
+ offset + ATOMIC_COUNTER_OFFSET, ATOMIC_COUNTER_SIZE)) {
+ throw new IllegalStateException("cannot read shared memory, type " + memType);
+ }
+
+ if (lastValid) {
+ boolean failed = false;
+ int i;
+ for (i = 0; i < SENSORS_EVENT_SIZE; ++i) {
+ if (lastArray[i] != mBuffer[lastOffset + i]) {
+ failed = true;
+ break;
+ }
+ }
+
+ if (failed) {
+ byte[] currentArray = new byte[SENSORS_EVENT_SIZE];
+ System.arraycopy(mBuffer, lastOffset, currentArray, 0, SENSORS_EVENT_SIZE);
+
+ // wait for 100ms and read again to see if the change settle
+ try {
+ SensorCtsHelper.sleep(100, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+
+ byte[] delayedRead = new byte[SENSORS_EVENT_SIZE];
+ if (!readSharedMemory(
+ memType, false/*secondary*/, lastOffset, SENSORS_EVENT_SIZE)) {
+ throw new IllegalStateException(
+ "cannot read shared memory, type " + memType);
+ }
+ System.arraycopy(mBuffer, lastOffset, delayedRead, 0, SENSORS_EVENT_SIZE);
+
+ fail(String.format(
+ "At offset %d(0x%x), byte %d(0x%x) changed after atomicCounter"
+ + "(expecting %d, 0x%x) update, old = [%s], new = [%s], "
+ + "delayed = [%s]",
+ lastOffset, lastOffset, i, i, atomicCounter, atomicCounter,
+ SensorCtsHelper.bytesToHex(lastArray, -1, -1),
+ SensorCtsHelper.bytesToHex(currentArray, -1, -1),
+ SensorCtsHelper.bytesToHex(delayedRead, -1, -1)));
+ }
+ }
+
+ if (readAtomicCounter(offset) == atomicCounter) {
+ // read entire event again and parse
+ if (!readSharedMemory(memType, false/*secondary*/, offset, SENSORS_EVENT_SIZE)) {
+ throw new IllegalStateException("cannot read shared memory, type " + memType);
+ }
+ parseSensorEvent(offset, e);
+
+ atomicCounter += 1;
+
+ lastOffset = offset;
+ System.arraycopy(mBuffer, lastOffset, lastArray, 0, SENSORS_EVENT_SIZE);
+ lastValid = true;
+
+ offset += SENSORS_EVENT_SIZE;
+ if (offset + SENSORS_EVENT_SIZE > SHARED_MEMORY_SIZE) {
+ offset = 0;
+ }
+ }
+ }
+ Log.d(TAG, "at finish checkAtomicCounterUpdate has atomic counter = " + atomicCounter);
+ // atomicCounter will not wrap back in reasonable amount of time
+ assertTrue("Realtime event collection got no data", atomicCounter != 1);
+ }
+
private MemoryFile allocateMemoryFile() {
MemoryFile memFile = null;
try {
@@ -1019,6 +1397,76 @@
return minMax;
}
+ private void checkTimestampAbsolute(List<DirectReportSensorEvent> events) {
+ final int MAX_DETAIL_ITEM = 10;
+
+ StringBuffer buf = new StringBuffer();
+ int oneMsEarlyCount = 0;
+ int fiveMsLateCount = 0;
+ int tenMsLateCount = 0;
+ int errorCount = 0;
+
+ for (int i = 0; i < events.size(); ++i) {
+ DirectReportSensorEvent e = events.get(i);
+ long d = e.arrivalTs - e.ts;
+ boolean oneMsEarly = d < -1000_000;
+ boolean fiveMsLate = d > 5000_000;
+ boolean tenMsLate = d > 10_000_000;
+
+ if (oneMsEarly || fiveMsLate || tenMsLate) {
+ oneMsEarlyCount += oneMsEarly ? 1 : 0;
+ fiveMsLateCount += fiveMsLate ? 1 : 0;
+ tenMsLateCount += tenMsLate ? 1 : 0;
+
+ if (errorCount++ < MAX_DETAIL_ITEM) {
+ buf.append("[").append(i).append("] diff = ").append(d / 1e6f).append(" ms; ");
+ }
+ }
+ }
+
+ Log.d(TAG, String.format("Irregular timestamp, %d, %d, %d out of %d",
+ oneMsEarlyCount, fiveMsLateCount, tenMsLateCount, events.size()));
+
+ if (CHECK_ABSOLUTE_LATENCY) {
+ assertTrue(String.format(
+ "Timestamp error, out of %d events, %d is >1ms early, %d is >5ms late, "
+ + "%d is >10ms late, details: %s%s",
+ events.size(), oneMsEarlyCount, fiveMsLateCount, tenMsLateCount,
+ buf.toString(), errorCount > MAX_DETAIL_ITEM ? "..." : ""),
+ oneMsEarlyCount == 0
+ && fiveMsLateCount <= events.size() / 20
+ && tenMsLateCount <= events.size() / 100);
+ }
+ }
+
+ private void checkTimestampRelative(List<DirectReportSensorEvent> directEvents,
+ List<DirectReportSensorEvent> pollEvents) {
+ if (directEvents.size() < 10 || pollEvents.size() < 10) {
+ // cannot check with so few data points
+ return;
+ }
+
+ long directAverageLatency = 0;
+ for (DirectReportSensorEvent e : directEvents) {
+ directAverageLatency += e.arrivalTs - e.ts;
+ }
+ directAverageLatency /= directEvents.size();
+
+ long pollAverageLatency = 0;
+ for (DirectReportSensorEvent e : pollEvents) {
+ pollAverageLatency += e.arrivalTs - e.ts;
+ }
+ pollAverageLatency /= pollEvents.size();
+
+ Log.d(TAG, String.format("Direct, poll latency = %f, %f ms",
+ directAverageLatency / 1e6f, pollAverageLatency / 1e6f));
+ assertTrue(
+ String.format("Direct, poll latency = %f, %f ms, expect direct < poll",
+ directAverageLatency / 1e6f,
+ pollAverageLatency / 1e6f),
+ directAverageLatency < pollAverageLatency + 1000_000);
+ }
+
private int[] calculateExpectedNEventsUs(int timeMs, int samplingPeriodUs) {
int[] minMax = new int[2];
minMax[0] = Math.max((int) Math.floor(
@@ -1172,4 +1620,32 @@
ev.y = mByteBuffer.getFloat();
ev.z = mByteBuffer.getFloat();
}
+
+ // parse sensors_event_t and fill information into DirectReportSensorEvent
+ private static void parseSensorEvent(byte [] buf, int offset, DirectReportSensorEvent ev) {
+ ByteBuffer b = ByteBuffer.wrap(buf, offset, SENSORS_EVENT_SIZE);
+ b.order(NATIVE_BYTE_ORDER);
+
+ ev.size = b.getInt();
+ ev.token = b.getInt();
+ ev.type = b.getInt();
+ ev.serial = ((long) b.getInt()) & 0xFFFFFFFFl; // signed=>unsigned
+ ev.ts = b.getLong();
+ ev.arrivalTs = SystemClock.elapsedRealtimeNanos();
+ ev.x = b.getFloat();
+ ev.y = b.getFloat();
+ ev.z = b.getFloat();
+ }
+
+ private long readAtomicCounter(int offset) {
+ mByteBuffer.position(offset + ATOMIC_COUNTER_OFFSET);
+ return ((long) mByteBuffer.getInt()) & 0xFFFFFFFFl; // signed => unsigned
+ }
+
+ private static long readAtomicCounter(byte [] buf, int offset) {
+ ByteBuffer b = ByteBuffer.wrap(buf, offset + ATOMIC_COUNTER_OFFSET, ATOMIC_COUNTER_SIZE);
+ b.order(ByteOrder.nativeOrder());
+
+ return ((long) b.getInt()) & 0xFFFFFFFFl; // signed => unsigned
+ }
}