am d9d0ba5c: am 0ec02670: Merge "Save all sensor events when a verification fails. b/17838681" into lmp-mr1-dev

* commit 'd9d0ba5cde8104dfc4e4164121f253409a14d15e':
  Save all sensor events when a verification fails. b/17838681
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
index 4a0c135..52b3dee 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
@@ -101,7 +101,7 @@
         verifyMeasurements.addVerification(new MeanVerification(
                 expectations,
                 new float[]{1.95f, 1.95f, 1.95f} /* m / s^2 */));
-        verifyMeasurements.execute();
+        verifyMeasurements.execute(getCurrentTestNode());
         return null;
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
index 2d58c64..7ef63d7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
@@ -150,8 +150,7 @@
 
     private String executeTest(TestSensorOperation operation) throws InterruptedException {
         operation.addDefaultVerifications();
-        operation.setLogEvents(true);
-        operation.execute();
+        operation.execute(getCurrentTestNode());
         return null;
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
index d6ec951..7be0fb1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
@@ -167,7 +167,7 @@
         sensorOperation.addVerification(integrationVerification);
 
         try {
-            sensorOperation.execute();
+            sensorOperation.execute(getCurrentTestNode());
         } finally {
             playSound();
         }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
index 22ff1e5..229a9dc 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
@@ -87,7 +87,7 @@
         verifyNorm.addVerification(new MagnitudeVerification(
                 expectedMagneticFieldEarth,
                 magneticFieldEarthThreshold));
-        verifyNorm.execute();
+        verifyNorm.execute(getCurrentTestNode());
         return null;
     }
 
@@ -127,7 +127,7 @@
 
         verifyStdDev.addVerification(new StandardDeviationVerification(
                 new float[]{2f, 2f, 2f} /* uT */));
-        verifyStdDev.execute();
+        verifyStdDev.execute(getCurrentTestNode());
         return null;
     }
 
@@ -169,6 +169,7 @@
                 getApplicationContext(),
                 Sensor.TYPE_MAGNETIC_FIELD,
                 SensorManager.SENSOR_DELAY_NORMAL);
+
         TestSensorEventListener listener = new TestSensorEventListener(environment) {
             @Override
             public void onSensorChanged(SensorEvent event) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
index 5bbaaf7..380b282 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
@@ -26,6 +26,8 @@
 
 import android.content.Context;
 import android.hardware.cts.SensorTestCase;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.Enumeration;
 
@@ -138,10 +140,24 @@
             SensorTestCase sensorTestCase = (SensorTestCase) testCase;
             sensorTestCase.setContext(mContext);
             sensorTestCase.setEmulateSensorUnderLoad(false);
+            sensorTestCase.setCurrentTestNode(new TestNode(testCase));
             // TODO: set delayed assertion provider
         } else {
             throw new IllegalStateException("TestCase must be an instance of SensorTestCase.");
         }
         super.run(testCase);
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final TestCase mTestCase;
+
+        public TestNode(TestCase testCase) {
+            mTestCase = testCase;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return mTestCase.getClass().getSimpleName() + "_" + mTestCase.getName();
+        }
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
index a88abd0..8cf287a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
@@ -19,6 +19,8 @@
 
 import com.android.cts.verifier.sensors.reporting.SensorTestDetails;
 
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -35,6 +37,7 @@
     private volatile int mTestPassedCounter;
     private volatile int mTestSkippedCounter;
     private volatile int mTestFailedCounter;
+    private volatile ISensorTestNode mCurrentTestNode;
 
     /**
      * {@inheritDoc}
@@ -63,8 +66,12 @@
                 mTestFailedCounter);
     }
 
+    protected ISensorTestNode getCurrentTestNode() {
+        return mCurrentTestNode;
+    }
+
     private List<Method> findTestMethods() {
-        ArrayList<Method> testMethods = new ArrayList<Method>();
+        ArrayList<Method> testMethods = new ArrayList<>();
         for (Method method : mTestClass.getDeclaredMethods()) {
             if (Modifier.isPublic(method.getModifiers())
                     && method.getParameterTypes().length == 0
@@ -79,6 +86,7 @@
     private SensorTestDetails executeTest(Method testMethod) throws InterruptedException {
         String testMethodName = testMethod.getName();
         String testName = String.format("%s#%s", getTestClassName(), testMethodName);
+        mCurrentTestNode = new TestNode(testMethod);
 
         SensorTestDetails testDetails;
         try {
@@ -112,4 +120,17 @@
 
         return testDetails;
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final Method mTestMethod;
+
+        public TestNode(Method testMethod) {
+            mTestMethod = testMethod;
+        }
+
+        @Override
+        public String getName() {
+            return mTestClass.getSimpleName() + "_" + mTestMethod.getName();
+        }
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java b/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
index e2b7561..7640cd7 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
@@ -288,10 +288,9 @@
             TestSensorOperation operation,
             boolean flushExpected) throws Throwable {
         operation.addDefaultVerifications();
-        operation.setLogEvents(true);
 
         try {
-            operation.execute();
+            operation.execute(getCurrentTestNode());
         } finally {
             SensorStats stats = operation.getStats();
             stats.log(TAG);
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
index 1f070c4..8c3fb7a 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
@@ -18,7 +18,6 @@
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
-import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.sensoroperations.ParallelSensorOperation;
 import android.hardware.cts.helpers.sensoroperations.RepeatingSensorOperation;
@@ -96,7 +95,7 @@
             batchingOperation.addVerification(new EventOrderingVerification());
             operation.add(new RepeatingSensorOperation(batchingOperation, ITERATIONS));
         }
-        operation.execute();
+        operation.execute(getCurrentTestNode());
         operation.getStats().log(TAG);
     }
 
@@ -152,7 +151,7 @@
             }
         }
 
-        operation.execute();
+        operation.execute(getCurrentTestNode());
         operation.getStats().log(TAG);
     }
 
@@ -242,11 +241,11 @@
 
         ParallelSensorOperation operation = new ParallelSensorOperation();
         operation.add(tester, testee);
-        operation.execute();
+        operation.execute(getCurrentTestNode());
         operation.getStats().log(TAG);
 
         testee = testee.clone();
-        testee.execute();
+        testee.execute(getCurrentTestNode());
         testee.getStats().log(TAG);
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorTest.java b/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
index 87c74ee..c16c135 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
@@ -364,7 +364,7 @@
         }
 
         Log.i(TAG, "Testing batch/flush for sensors: " + builder);
-        parallelSensorOperation.execute();
+        parallelSensorOperation.execute(getCurrentTestNode());
     }
 
     private void assertSensorValues(Sensor sensor) {
@@ -436,7 +436,7 @@
                     EventTimestampSynchronizationVerification.getDefault(environment));
 
             Log.i(TAG, "Running timestamp test on: " + sensor.getName());
-            operation.execute();
+            operation.execute(getCurrentTestNode());
         } catch (InterruptedException e) {
             // propagate so the test can stop
             throw e;
@@ -471,7 +471,7 @@
             TestSensorOperation operation = new TestSensorOperation(environment, executor, handler);
 
             Log.i(TAG, "Running flush test on: " + sensor.getName());
-            operation.execute();
+            operation.execute(getCurrentTestNode());
         } catch (InterruptedException e) {
             // propagate so the test can stop
             throw e;
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java b/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
index 8dba5d6..42b8d33 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
@@ -16,16 +16,10 @@
 
 package android.hardware.cts;
 
-import com.android.cts.util.ReportLog;
-import com.android.cts.util.ResultType;
-import com.android.cts.util.ResultUnit;
-
-import android.app.Instrumentation;
-import android.cts.util.DeviceReportLog;
 import android.hardware.Sensor;
-import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
 import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.hardware.cts.helpers.sensoroperations.SensorOperation;
 import android.test.AndroidTestCase;
 import android.util.Log;
@@ -48,6 +42,11 @@
      */
     private volatile boolean mEmulateSensorUnderLoad = true;
 
+    /**
+     * By default the test class is the root of the test hierarchy.
+     */
+    private volatile ISensorTestNode mCurrentTestNode = new TestClassNode(getClass());
+
     protected SensorTestCase() {}
 
     @Override
@@ -68,33 +67,24 @@
         return mEmulateSensorUnderLoad;
     }
 
-    /**
-     * Utility method to log selected stats to a {@link ReportLog} object.  The stats must be
-     * a number or an array of numbers.
-     */
-    public static void logSelectedStatsToReportLog(Instrumentation instrumentation, int depth,
-            String[] keys, SensorStats stats) {
-        DeviceReportLog reportLog = new DeviceReportLog(depth);
+    public void setCurrentTestNode(ISensorTestNode value) {
+        mCurrentTestNode = value;
+    }
 
-        for (String key : keys) {
-            Object value = stats.getValue(key);
-            if (value instanceof Integer) {
-                reportLog.printValue(key, (Integer) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof Double) {
-                reportLog.printValue(key, (Double) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof Float) {
-                reportLog.printValue(key, (Float) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof double[]) {
-                reportLog.printArray(key, (double[]) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof float[]) {
-                float[] tmpFloat = (float[]) value;
-                double[] tmpDouble = new double[tmpFloat.length];
-                for (int i = 0; i < tmpDouble.length; i++) tmpDouble[i] = tmpFloat[i];
-                reportLog.printArray(key, tmpDouble, ResultType.NEUTRAL, ResultUnit.NONE);
-            }
+    protected ISensorTestNode getCurrentTestNode() {
+        return mCurrentTestNode;
+    }
+
+    private class TestClassNode implements ISensorTestNode {
+        private final Class<?> mTestClass;
+
+        public TestClassNode(Class<?> testClass) {
+            mTestClass = testClass;
         }
 
-        reportLog.printSummary("summary", 0, ResultType.NEUTRAL, ResultUnit.NONE);
-        reportLog.deliverReportToHost(instrumentation);
+        @Override
+        public String getName() {
+            return mTestClass.getSimpleName();
+        }
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
index 23feef8..42cbdfb 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
@@ -544,9 +544,9 @@
         TestSensorOperation op =
                 TestSensorOperation.createOperation(environment, 5, TimeUnit.SECONDS);
         op.addDefaultVerifications();
-        op.setLogEvents(true);
+
         try {
-            op.execute();
+            op.execute(getCurrentTestNode());
         } finally {
             SensorStats stats = op.getStats();
             stats.log(TAG);
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
index e8500f1..86b2436 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
@@ -21,6 +21,8 @@
 import android.hardware.SensorEventListener2;
 import android.os.SystemClock;
 
+import java.util.Arrays;
+
 /**
  * Class for holding information about individual {@link SensorEvent}s.
  */
@@ -75,4 +77,14 @@
         this.accuracy = accuracy;
         this.values = values;
     }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "Timestamp=%sns, ReceivedTimestamp=%sns, Accuracy=%s, Values=%s",
+                this.timestamp,
+                this.receivedTimestamp,
+                this.accuracy,
+                Arrays.toString(this.values));
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
index 6c313d2..a60428f 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
@@ -25,7 +25,12 @@
 import android.os.Looper;
 import android.os.SystemClock;
 
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -40,6 +45,7 @@
  */
 public class TestSensorEventListener implements SensorEventListener2 {
     public static final String LOG_TAG = "TestSensorEventListener";
+
     private static final long EVENT_TIMEOUT_US = TimeUnit.SECONDS.toMicros(5);
     private static final long FLUSH_TIMEOUT_US = TimeUnit.SECONDS.toMicros(10);
 
@@ -84,7 +90,6 @@
         synchronized (mCollectedEvents) {
             mCollectedEvents.add(new TestSensorEvent(event, timestampNs));
         }
-
         synchronized (mEventLatches) {
             for (CountDownLatch latch : mEventLatches) {
                 latch.countDown();
@@ -138,6 +143,40 @@
         }
     }
 
+
+    /**
+     * Utility method to log the collected events to a file.
+     * It will overwrite the file if it already exists, the file is created in a relative directory
+     * named 'events' under the sensor test directory (part of external storage).
+     */
+    public void logCollectedEventsToFile(String fileName) throws IOException {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Sensor='").append(mEnvironment.getSensor()).append("', ");
+        builder.append("SamplingRateOverloaded=")
+                .append(mEnvironment.isSensorSamplingRateOverloaded()).append(", ");
+        builder.append("RequestedSamplingPeriod=")
+                .append(mEnvironment.getRequestedSamplingPeriodUs()).append("us, ");
+        builder.append("MaxReportLatency=")
+                .append(mEnvironment.getMaxReportLatencyUs()).append("us");
+
+        synchronized (mCollectedEvents) {
+            for (TestSensorEvent event : mCollectedEvents) {
+                builder.append("\n");
+                builder.append("Timestamp=").append(event.timestamp).append("ns, ");
+                builder.append("ReceivedTimestamp=").append(event.receivedTimestamp).append("ns, ");
+                builder.append("Accuracy=").append(event.accuracy).append(", ");
+                builder.append("Values=").append(Arrays.toString(event.values));
+            }
+        }
+
+        File eventsDirectory = SensorCtsHelper.getSensorTestDataDirectory("events/");
+        File logFile = new File(eventsDirectory, fileName);
+        FileWriter fileWriter = new FileWriter(logFile, false /* append */);
+        try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
+            writer.write(builder.toString());
+        }
+    }
+
     /**
      * Wait for {@link #onFlushCompleted(Sensor)} to be called.
      *
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java b/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java
new file mode 100644
index 0000000..d34c0af
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.hardware.cts.helpers.reporting;
+
+import android.hardware.cts.helpers.SensorTestPlatformException;
+
+/**
+ * Interface that represents a node in a hierarchy built by the sensor test platform.
+ */
+// TODO: this is an intermediate state to introduce a full-blown centralized recorder data produced
+//       by sensor tests
+public interface ISensorTestNode {
+
+    /**
+     * Provides a name (tag) that can be used to identify the current node.
+     */
+    String getName() throws SensorTestPlatformException;
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
index 8848337..436a7cf 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
@@ -22,7 +22,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 
@@ -76,7 +76,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         // Start alarm
         IntentFilter intentFilter = new IntentFilter(ACTION);
         BroadcastReceiver receiver = new BroadcastReceiver() {
@@ -96,7 +96,7 @@
 
         // Execute operation
         try {
-            mOperation.execute();
+            mOperation.execute(asTestNode(parent));
         } finally {
             releaseWakeLock();
         }
@@ -106,14 +106,6 @@
      * {@inheritDoc}
      */
     @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
     public AlarmOperation clone() {
         return new AlarmOperation(mOperation, mContext, mSleepDuration, mTimeUnit);
     }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
index 3ee08f6..8c52222 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
@@ -17,7 +17,7 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorCtsHelper;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.concurrent.TimeUnit;
 
@@ -38,9 +38,7 @@
      * @param timeUnit the unit of the delay
      */
     public DelaySensorOperation(SensorOperation operation, long delay, TimeUnit timeUnit) {
-        if (operation == null || timeUnit == null) {
-            throw new IllegalArgumentException("Arguments cannot be null");
-        }
+        super(operation.getStats());
         mOperation = operation;
         mDelay = delay;
         mTimeUnit = timeUnit;
@@ -50,17 +48,9 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         SensorCtsHelper.sleep(mDelay, mTimeUnit);
-        mOperation.execute();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
+        mOperation.execute(asTestNode(parent));
     }
 
     /**
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
index 8cfd351..238956b 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
@@ -16,9 +16,10 @@
 
 package android.hardware.cts.helpers.sensoroperations;
 
-import junit.framework.Assert;
-
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+
+import junit.framework.Assert;
 
 import java.util.concurrent.TimeUnit;
 
@@ -56,7 +57,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() {
+    public void execute(ISensorTestNode parent) {
         long delayNs = TimeUnit.NANOSECONDS.convert(mDelay, mTimeUnit);
         try {
             Thread.sleep(delayNs / NANOS_PER_MILLI, (int) (delayNs % NANOS_PER_MILLI));
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
index e55c6cb..ed70b70 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
@@ -19,6 +19,7 @@
 import junit.framework.Assert;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.os.SystemClock;
 
 import java.util.ArrayList;
@@ -80,7 +81,7 @@
      * operations, the first exception will be thrown once all operations are completed.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(final ISensorTestNode parent) throws InterruptedException {
         int operationsCount = mOperations.size();
         ThreadPoolExecutor executor = new ThreadPoolExecutor(
                 operationsCount,
@@ -91,12 +92,13 @@
         executor.allowCoreThreadTimeOut(true);
         executor.prestartAllCoreThreads();
 
+        final ISensorTestNode currentNode = asTestNode(parent);
         ArrayList<Future<SensorOperation>> futures = new ArrayList<>();
         for (final SensorOperation operation : mOperations) {
             Future<SensorOperation> future = executor.submit(new Callable<SensorOperation>() {
                 @Override
                 public SensorOperation call() throws Exception {
-                    operation.execute();
+                    operation.execute(currentNode);
                     return operation;
                 }
             });
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
index 2e3af36..5b333b8 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
@@ -17,6 +17,8 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 /**
  * A {@link SensorOperation} that executes a single {@link SensorOperation} a given number of
@@ -40,7 +42,6 @@
         }
         mOperation = operation;
         mIterations = iterations;
-
     }
 
     /**
@@ -48,11 +49,12 @@
      * one iterations, it is thrown and all subsequent iterations will not run.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
+        ISensorTestNode currentNode = asTestNode(parent);
         for(int i = 0; i < mIterations; ++i) {
             SensorOperation operation = mOperation.clone();
             try {
-                operation.execute();
+                operation.execute(new TestNode(currentNode, i));
             } catch (AssertionError e) {
                 String msg = String.format("Iteration %d failed: \"%s\"", i, e.getMessage());
                 getStats().addValue(SensorStats.ERROR, msg);
@@ -70,4 +72,19 @@
     public RepeatingSensorOperation clone() {
         return new RepeatingSensorOperation(mOperation.clone(), mIterations);
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final ISensorTestNode mTestNode;
+        private final int mIteration;
+
+        public TestNode(ISensorTestNode parent, int iteration) {
+            mTestNode = asTestNode(parent);
+            mIteration = iteration;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return String.format("%s-iteration%d", mTestNode.getName(), mIteration);
+        }
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java
index ea16716..66604d3 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java
@@ -17,6 +17,8 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 /**
  * Base class used by all sensor operations. This allows for complex operations such as chaining
@@ -24,11 +26,12 @@
  * <p>
  * Certain restrictions exist for {@link SensorOperation}s:
  * <p><ul>
- * <li>{@link #execute()} should only be called once and behavior is undefined for subsequent calls.
- * Once {@link #execute()} is called, the class should not be modified. Generally, there is no
- * synchronization for operations.</li>
- * <li>{@link #getStats()} should only be called after {@link #execute()}. If it is called before,
- * the returned value is undefined.</li>
+ * <li>{@link #execute(ISensorTestNode)} should only be called once and behavior is undefined for
+ * subsequent calls.
+ * Once {@link #execute(ISensorTestNode)} is called, the class should not be modified. Generally,
+ * there is no synchronization for operations.</li>
+ * <li>{@link #getStats()} should only be called after {@link #execute(ISensorTestNode)}. If it
+ * is called before, the returned value is undefined.</li>
  * <li>{@link #clone()} may be called any time and should return an operation with the same
  * parameters as the original.</li>
  * </ul>
@@ -59,7 +62,7 @@
      * - cleaning up on {@link InterruptedException}
      * - propagating the exception down the stack
      */
-    public abstract void execute() throws InterruptedException;
+    public abstract void execute(ISensorTestNode parent) throws InterruptedException;
 
     /**
      * @return The cloned {@link SensorOperation}.
@@ -85,4 +88,23 @@
     protected void addSensorStats(String key, int index, SensorStats stats) {
         addSensorStats(String.format("%s_%03d", key, index), stats);
     }
+
+    protected ISensorTestNode asTestNode(ISensorTestNode parent) {
+        return new SensorTestNode(parent, this);
+    }
+
+    private class SensorTestNode implements ISensorTestNode {
+        private final ISensorTestNode mParent;
+        private final SensorOperation mOperation;
+
+        public SensorTestNode(ISensorTestNode parent, SensorOperation operation) {
+            mParent = parent;
+            mOperation = operation;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return mParent.getName() + "-" + mOperation.getClass().getSimpleName();
+        }
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
index 033f3c5..30da9a0 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
@@ -19,6 +19,8 @@
 import junit.framework.TestCase;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -31,6 +33,13 @@
 public class SensorOperationTest extends TestCase {
     private static final long TEST_DURATION_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(5);
 
+    private final ISensorTestNode mTestNode = new ISensorTestNode() {
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return "SensorOperationUnitTest";
+        }
+    };
+
     /**
      * Test that the {@link FakeSensorOperation} functions correctly. Other tests in this class
      * rely on this operation.
@@ -42,14 +51,14 @@
 
         assertFalse(op.getStats().flatten().containsKey("executed"));
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(opDurationMs - duration) < TEST_DURATION_THRESHOLD_MS);
         assertTrue(op.getStats().flatten().containsKey("executed"));
 
         op = new FakeSensorOperation(true, 0, TimeUnit.MILLISECONDS);
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -68,7 +77,7 @@
         SensorOperation op = new DelaySensorOperation(subOp, opDurationMs, TimeUnit.MILLISECONDS);
 
         long startMs = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long durationMs = System.currentTimeMillis() - startMs;
         long durationDeltaMs = Math.abs(opDurationMs + subOpDurationMs - durationMs);
         assertTrue(durationDeltaMs < TEST_DURATION_THRESHOLD_MS);
@@ -92,7 +101,7 @@
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long durationMs = System.currentTimeMillis() - start;
         long durationDeltaMs = Math.abs(subOpDurationMs - durationMs);
         String message = String.format(
@@ -132,7 +141,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -172,7 +181,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -203,7 +212,7 @@
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(subOpDurationMs * iterations - duration) < TEST_DURATION_THRESHOLD_MS);
 
@@ -228,8 +237,8 @@
             private SensorStats mFakeStats = new SensorStats();
 
             @Override
-            public void execute() {
-                super.execute();
+            public void execute(ISensorTestNode parent) {
+                super.execute(parent);
                 mExecutedCount++;
 
                 if (failCount == mExecutedCount) {
@@ -255,7 +264,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -292,7 +301,7 @@
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(subOpDurationMs * subOpCount - duration) < TEST_DURATION_THRESHOLD_MS);
 
@@ -324,7 +333,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
index 85d189a..847c0f2 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
@@ -17,6 +17,7 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.ArrayList;
 
@@ -47,11 +48,12 @@
      * in one operation, it is thrown and all subsequent operations will not run.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
+        ISensorTestNode currentNode = asTestNode(parent);
         for (int i = 0; i < mOperations.size(); i++) {
             SensorOperation operation = mOperations.get(i);
             try {
-                operation.execute();
+                operation.execute(currentNode);
             } catch (AssertionError e) {
                 String msg = String.format("Operation %d failed: \"%s\"", i, e.getMessage());
                 getStats().addValue(SensorStats.ERROR, msg);
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
index 7347fc7..901216a 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
@@ -20,10 +20,12 @@
 
 import android.hardware.cts.helpers.SensorCtsHelper;
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 import android.hardware.cts.helpers.TestSensorEventListener;
 import android.hardware.cts.helpers.TestSensorManager;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.hardware.cts.helpers.sensorverification.EventGapVerification;
 import android.hardware.cts.helpers.sensorverification.EventOrderingVerification;
 import android.hardware.cts.helpers.sensorverification.EventTimestampSynchronizationVerification;
@@ -34,7 +36,9 @@
 import android.hardware.cts.helpers.sensorverification.MeanVerification;
 import android.hardware.cts.helpers.sensorverification.StandardDeviationVerification;
 import android.os.Handler;
+import android.util.Log;
 
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -43,11 +47,13 @@
  * A {@link SensorOperation} used to verify that sensor events and sensor values are correct.
  * <p>
  * Provides methods to set test expectations as well as providing a set of default expectations
- * depending on sensor type.  When {{@link #execute()} is called, the sensor will collect the
- * events and then run all the tests.
+ * depending on sensor type.  When {{@link #execute(ISensorTestNode)} is called, the sensor will
+ * collect the events and then run all the tests.
  * </p>
  */
 public class TestSensorOperation extends SensorOperation {
+    private static final String TAG = "TestSensorOperation";
+
     private final HashSet<ISensorVerification> mVerifications = new HashSet<>();
 
     private final TestSensorManager mSensorManager;
@@ -55,8 +61,6 @@
     private final Executor mExecutor;
     private final Handler mHandler;
 
-    private boolean mLogEvents;
-
     /**
      * An interface that defines an abstraction for operations to be performed by the
      * {@link TestSensorOperation}.
@@ -87,13 +91,6 @@
     }
 
     /**
-     * Set whether to log events.
-     */
-    public void setLogEvents(boolean logEvents) {
-        mLogEvents = logEvents;
-    }
-
-    /**
      * Set all of the default test expectations.
      */
     public void addDefaultVerifications() {
@@ -117,10 +114,9 @@
      * Collect the specified number of events from the sensor and run all enabled verifications.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         getStats().addValue("sensor_name", mEnvironment.getSensor().getName());
         TestSensorEventListener listener = new TestSensorEventListener(mEnvironment, mHandler);
-
         mExecutor.execute(mSensorManager, listener);
 
         boolean failed = false;
@@ -131,6 +127,8 @@
         }
 
         if (failed) {
+            trySaveCollectedEvents(parent, listener);
+
             String msg = SensorCtsHelper
                     .formatAssertionMessage("VerifySensorOperation", mEnvironment, sb.toString());
             getStats().addValue(SensorStats.ERROR, msg);
@@ -173,6 +171,34 @@
     }
 
     /**
+     * Tries to save collected {@link TestSensorEvent}s to a file.
+     *
+     * NOTE: it is more important to handle verifications and its results, than failing if the file
+     * cannot be created. So we silently fail if necessary.
+     */
+    private void trySaveCollectedEvents(ISensorTestNode parent, TestSensorEventListener listener) {
+        String sanitizedFileName;
+        try {
+            String fileName = asTestNode(parent).getName();
+            sanitizedFileName = String.format(
+                    "%s-%s-%s_%dus.txt",
+                    SensorCtsHelper.sanitizeStringForFileName(fileName),
+                    SensorStats.getSanitizedSensorName(mEnvironment.getSensor()),
+                    mEnvironment.getFrequencyString(),
+                    mEnvironment.getMaxReportLatencyUs());
+        } catch (SensorTestPlatformException e) {
+            Log.w(TAG, "Unable to generate file name to save collected events", e);
+            return;
+        }
+
+        try {
+            listener.logCollectedEventsToFile(sanitizedFileName);
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to save collected events to file: " + sanitizedFileName, e);
+        }
+    }
+
+    /**
      * Creates an operation that will wait for a given amount of events to arrive.
      *
      * @param environment The test environment.
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
index 5f4f5d8..9f03f31 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
@@ -17,7 +17,7 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.content.Context;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 
@@ -40,6 +40,7 @@
      * @param wakeLockFlags the flags used when acquiring the wake-lock
      */
     public WakeLockOperation(SensorOperation operation, Context context, int wakeLockFlags) {
+        super(operation.getStats());
         mOperation = operation;
         mContext = context;
         mWakeLockFlags = wakeLockFlags;
@@ -59,13 +60,12 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         WakeLock wakeLock = pm.newWakeLock(mWakeLockFlags, TAG);
-
         wakeLock.acquire();
         try {
-            mOperation.execute();
+            mOperation.execute(asTestNode(parent));
         } finally {
             wakeLock.release();
         }
@@ -75,15 +75,7 @@
      * {@inheritDoc}
      */
     @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
     public SensorOperation clone() {
-        return new WakeLockOperation(mOperation, mContext, mWakeLockFlags);
+        return new WakeLockOperation(mOperation.clone(), mContext, mWakeLockFlags);
     }
 }