Home | History | Annotate | Download | only in helpers
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.hardware.cts.helpers;
     17 
     18 import android.hardware.Sensor;
     19 import android.os.Environment;
     20 import android.util.Log;
     21 import java.io.File;
     22 import java.io.IOException;
     23 import java.util.ArrayList;
     24 import java.util.Collection;
     25 import java.util.Collections;
     26 import java.util.List;
     27 import java.util.concurrent.TimeUnit;
     28 
     29 /**
     30  * Set of static helper methods for CTS tests.
     31  */
     32 //TODO: Refactor this class into several more well defined helper classes, look at StatisticsUtils
     33 public class SensorCtsHelper {
     34 
     35     private static final long NANOS_PER_MILLI = 1000000;
     36 
     37     /**
     38      * Private constructor for static class.
     39      */
     40     private SensorCtsHelper() {}
     41 
     42     /**
     43      * Get low and high percentiles values of an array
     44      *
     45      * @param lowPercentile Lower boundary percentile, range [0, 1]
     46      * @param highPercentile Higher boundary percentile, range [0, 1]
     47      *
     48      * @throws IllegalArgumentException if the collection or percentiles is null or empty.
     49      */
     50     public static <TValue extends Comparable<? super TValue>> List<TValue> getPercentileValue(
     51             Collection<TValue> collection, float lowPecentile, float highPercentile) {
     52         validateCollection(collection);
     53         if (lowPecentile > highPercentile || lowPecentile < 0 || highPercentile > 1) {
     54             throw new IllegalStateException("percentile has to be in range [0, 1], and " +
     55                     "lowPecentile has to be less than or equal to highPercentile");
     56         }
     57 
     58         List<TValue> arrayCopy = new ArrayList<TValue>(collection);
     59         Collections.sort(arrayCopy);
     60 
     61         List<TValue> percentileValues = new ArrayList<TValue>();
     62         // lower percentile: rounding upwards, index range 1 .. size - 1 for percentile > 0
     63         // for percentile == 0, index will be 0.
     64         int lowArrayIndex = Math.min(arrayCopy.size() - 1,
     65                 arrayCopy.size() - (int)(arrayCopy.size() * (1 - lowPecentile)));
     66         percentileValues.add(arrayCopy.get(lowArrayIndex));
     67 
     68         // upper percentile: rounding downwards, index range 0 .. size - 2 for percentile < 1
     69         // for percentile == 1, index will be size - 1.
     70         // Also, lower bound by lowerArrayIndex to avoid low percentile value being higher than
     71         // high percentile value.
     72         int highArrayIndex = Math.max(lowArrayIndex, (int)(arrayCopy.size() * highPercentile - 1));
     73         percentileValues.add(arrayCopy.get(highArrayIndex));
     74         return percentileValues;
     75     }
     76 
     77     /**
     78      * Calculate the mean of a collection.
     79      *
     80      * @throws IllegalArgumentException if the collection is null or empty
     81      */
     82     public static <TValue extends Number> double getMean(Collection<TValue> collection) {
     83         validateCollection(collection);
     84 
     85         double sum = 0.0;
     86         for(TValue value : collection) {
     87             sum += value.doubleValue();
     88         }
     89         return sum / collection.size();
     90     }
     91 
     92     /**
     93      * Calculate the bias-corrected sample variance of a collection.
     94      *
     95      * @throws IllegalArgumentException if the collection is null or empty
     96      */
     97     public static <TValue extends Number> double getVariance(Collection<TValue> collection) {
     98         validateCollection(collection);
     99 
    100         double mean = getMean(collection);
    101         ArrayList<Double> squaredDiffs = new ArrayList<Double>();
    102         for(TValue value : collection) {
    103             double difference = mean - value.doubleValue();
    104             squaredDiffs.add(Math.pow(difference, 2));
    105         }
    106 
    107         double sum = 0.0;
    108         for (Double value : squaredDiffs) {
    109             sum += value;
    110         }
    111         return sum / (squaredDiffs.size() - 1);
    112     }
    113 
    114     /**
    115      * @return The (measured) sampling rate of a collection of {@link TestSensorEvent}.
    116      */
    117     public static long getSamplingPeriodNs(List<TestSensorEvent> collection) {
    118         int collectionSize = collection.size();
    119         if (collectionSize < 2) {
    120             return 0;
    121         }
    122         TestSensorEvent firstEvent = collection.get(0);
    123         TestSensorEvent lastEvent = collection.get(collectionSize - 1);
    124         return (lastEvent.timestamp - firstEvent.timestamp) / (collectionSize - 1);
    125     }
    126 
    127     /**
    128      * Calculate the bias-corrected standard deviation of a collection.
    129      *
    130      * @throws IllegalArgumentException if the collection is null or empty
    131      */
    132     public static <TValue extends Number> double getStandardDeviation(
    133             Collection<TValue> collection) {
    134         return Math.sqrt(getVariance(collection));
    135     }
    136 
    137     /**
    138      * Convert a period to frequency in Hz.
    139      */
    140     public static <TValue extends Number> double getFrequency(TValue period, TimeUnit unit) {
    141         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * period.doubleValue());
    142     }
    143 
    144     /**
    145      * Convert a frequency in Hz into a period.
    146      */
    147     public static <TValue extends Number> double getPeriod(TValue frequency, TimeUnit unit) {
    148         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * frequency.doubleValue());
    149     }
    150 
    151     /**
    152      * If value lies outside the boundary limit, then return the nearer bound value.
    153      * Otherwise, return the value unchanged.
    154      */
    155     public static <TValue extends Number> double clamp(TValue val, TValue min, TValue max) {
    156         return Math.min(max.doubleValue(), Math.max(min.doubleValue(), val.doubleValue()));
    157     }
    158 
    159     /**
    160      * @return The magnitude (norm) represented by the given array of values.
    161      */
    162     public static double getMagnitude(float[] values) {
    163         float sumOfSquares = 0.0f;
    164         for (float value : values) {
    165             sumOfSquares += value * value;
    166         }
    167         double magnitude = Math.sqrt(sumOfSquares);
    168         return magnitude;
    169     }
    170 
    171     /**
    172      * Helper method to sleep for a given duration.
    173      */
    174     public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException {
    175         long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit);
    176         Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
    177     }
    178 
    179     /**
    180      * Format an assertion message.
    181      *
    182      * @param label the verification name
    183      * @param environment the environment of the test
    184      *
    185      * @return The formatted string
    186      */
    187     public static String formatAssertionMessage(String label, TestSensorEnvironment environment) {
    188         return formatAssertionMessage(label, environment, "Failed");
    189     }
    190 
    191     /**
    192      * Format an assertion message with a custom message.
    193      *
    194      * @param label the verification name
    195      * @param environment the environment of the test
    196      * @param format the additional format string
    197      * @param params the additional format params
    198      *
    199      * @return The formatted string
    200      */
    201     public static String formatAssertionMessage(
    202             String label,
    203             TestSensorEnvironment environment,
    204             String format,
    205             Object ... params) {
    206         return formatAssertionMessage(label, environment, String.format(format, params));
    207     }
    208 
    209     /**
    210      * Format an assertion message.
    211      *
    212      * @param label the verification name
    213      * @param environment the environment of the test
    214      * @param extras the additional information for the assertion
    215      *
    216      * @return The formatted string
    217      */
    218     public static String formatAssertionMessage(
    219             String label,
    220             TestSensorEnvironment environment,
    221             String extras) {
    222         return String.format(
    223                 "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s",
    224                 label,
    225                 environment.getSensor().getName(),
    226                 environment.getRequestedSamplingPeriodUs(),
    227                 environment.getMaxReportLatencyUs(),
    228                 extras);
    229     }
    230 
    231     /**
    232      * Format an array of floats.
    233      *
    234      * @param array the array of floats
    235      *
    236      * @return The formatted string
    237      */
    238     public static String formatFloatArray(float[] array) {
    239         StringBuilder sb = new StringBuilder();
    240         if (array.length > 1) {
    241             sb.append("(");
    242         }
    243         for (int i = 0; i < array.length; i++) {
    244             sb.append(String.format("%.2f", array[i]));
    245             if (i != array.length - 1) {
    246                 sb.append(", ");
    247             }
    248         }
    249         if (array.length > 1) {
    250             sb.append(")");
    251         }
    252         return sb.toString();
    253     }
    254 
    255     /**
    256      * @return A {@link File} representing a root directory to store sensor tests data.
    257      */
    258     public static File getSensorTestDataDirectory() throws IOException {
    259         File dataDirectory = new File(Environment.getExternalStorageDirectory(), "sensorTests/");
    260         return createDirectoryStructure(dataDirectory);
    261     }
    262 
    263     /**
    264      * Creates the directory structure for the given sensor test data sub-directory.
    265      *
    266      * @param subdirectory The sub-directory's name.
    267      */
    268     public static File getSensorTestDataDirectory(String subdirectory) throws IOException {
    269         File subdirectoryFile = new File(getSensorTestDataDirectory(), subdirectory);
    270         return createDirectoryStructure(subdirectoryFile);
    271     }
    272 
    273     /**
    274      * Sanitizes a string so it can be used in file names.
    275      *
    276      * @param value The string to sanitize.
    277      * @return The sanitized string.
    278      *
    279      * @throws SensorTestPlatformException If the string cannot be sanitized.
    280      */
    281     public static String sanitizeStringForFileName(String value)
    282             throws SensorTestPlatformException {
    283         String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_");
    284         if (sanitizedValue.matches("_*")) {
    285             throw new SensorTestPlatformException(
    286                     "Unable to sanitize string '%s' for file name.",
    287                     value);
    288         }
    289         return sanitizedValue;
    290     }
    291 
    292     /**
    293      * Ensures that the directory structure represented by the given {@link File} is created.
    294      */
    295     private static File createDirectoryStructure(File directoryStructure) throws IOException {
    296         directoryStructure.mkdirs();
    297         if (!directoryStructure.isDirectory()) {
    298             throw new IOException("Unable to create directory structure for "
    299                     + directoryStructure.getAbsolutePath());
    300         }
    301         return directoryStructure;
    302     }
    303 
    304     /**
    305      * Validate that a collection is not null or empty.
    306      *
    307      * @throws IllegalStateException if collection is null or empty.
    308      */
    309     private static <T> void validateCollection(Collection<T> collection) {
    310         if(collection == null || collection.size() == 0) {
    311             throw new IllegalStateException("Collection cannot be null or empty");
    312         }
    313     }
    314 
    315     public static String getUnitsForSensor(Sensor sensor) {
    316         switch(sensor.getType()) {
    317             case Sensor.TYPE_ACCELEROMETER:
    318                 return "m/s^2";
    319             case Sensor.TYPE_MAGNETIC_FIELD:
    320             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
    321                 return "uT";
    322             case Sensor.TYPE_GYROSCOPE:
    323             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
    324                 return "radians/sec";
    325             case Sensor.TYPE_PRESSURE:
    326                 return "hPa";
    327         };
    328         return "";
    329     }
    330 
    331     public static boolean hasResolutionRequirement(Sensor sensor, boolean hasHifiSensors) {
    332         switch (sensor.getType()) {
    333             case Sensor.TYPE_ACCELEROMETER:
    334             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
    335             case Sensor.TYPE_GYROSCOPE:
    336             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
    337             case Sensor.TYPE_MAGNETIC_FIELD:
    338             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
    339                 return true;
    340 
    341             case Sensor.TYPE_PRESSURE:
    342                 // Pressure sensor only has a resolution requirement when there are HiFi sensors
    343                 return hasHifiSensors;
    344         }
    345         return false;
    346     }
    347 
    348     public static float getRequiredResolutionForSensor(Sensor sensor) {
    349         switch (sensor.getType()) {
    350             case Sensor.TYPE_ACCELEROMETER:
    351             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
    352             case Sensor.TYPE_GYROSCOPE:
    353             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
    354                 // Accelerometer and gyroscope must have at least 12 bits
    355                 // of resolution. The maximum resolution calculation uses
    356                 // slightly more than twice the maximum range because
    357                 //   1) the sensor must be able to report values from
    358                 //      [-maxRange, maxRange] without saturating
    359                 //   2) to allow for slight rounding errors
    360                 return (float)(2.001f * sensor.getMaximumRange() / Math.pow(2, 12));
    361             case Sensor.TYPE_MAGNETIC_FIELD:
    362             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
    363                 // Magnetometer must have a resolution equal to or denser
    364                 // than 0.6 uT
    365                 return 0.6f;
    366             case Sensor.TYPE_PRESSURE:
    367                 // Pressure sensor must have at least 80 LSB / hPa which is
    368                 // equivalent to 0.0125 hPa / LSB. Allow for a small margin of
    369                 // error due to rounding errors.
    370                 return 1.01f * (1.0f / 80.0f);
    371         }
    372         return 0.0f;
    373     }
    374 
    375     public static String sensorTypeShortString(int type) {
    376         switch (type) {
    377             case Sensor.TYPE_ACCELEROMETER:
    378                 return "Accel";
    379             case Sensor.TYPE_GYROSCOPE:
    380                 return "Gyro";
    381             case Sensor.TYPE_MAGNETIC_FIELD:
    382                 return "Mag";
    383             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
    384                 return "UncalAccel";
    385             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
    386                 return "UncalGyro";
    387             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
    388                 return "UncalMag";
    389             default:
    390                 return "Type_" + type;
    391         }
    392     }
    393 
    394     public static class TestResultCollector {
    395         private List<AssertionError> mErrorList = new ArrayList<>();
    396         private List<String> mErrorStringList = new ArrayList<>();
    397         private String mTestName;
    398         private String mTag;
    399 
    400         public TestResultCollector() {
    401             this("Test");
    402         }
    403 
    404         public TestResultCollector(String test) {
    405             this(test, "SensorCtsTest");
    406         }
    407 
    408         public TestResultCollector(String test, String tag) {
    409             mTestName = test;
    410             mTag = tag;
    411         }
    412 
    413         public void perform(Runnable r) {
    414             perform(r, "");
    415         }
    416 
    417         public void perform(Runnable r, String s) {
    418             try {
    419                 Log.d(mTag, mTestName + " running " + (s.isEmpty() ? "..." : s));
    420                 r.run();
    421             } catch (AssertionError e) {
    422                 mErrorList.add(e);
    423                 mErrorStringList.add(s);
    424                 Log.e(mTag, mTestName + " error: " + e.getMessage());
    425             }
    426         }
    427 
    428         public void judge() throws AssertionError {
    429             if (mErrorList.isEmpty() && mErrorStringList.isEmpty()) {
    430                 return;
    431             }
    432 
    433             if (mErrorList.size() != mErrorStringList.size()) {
    434                 throw new IllegalStateException("Mismatch error and error message");
    435             }
    436 
    437             StringBuffer buf = new StringBuffer();
    438             for (int i = 0; i < mErrorList.size(); ++i) {
    439                 buf.append("Test (").append(mErrorStringList.get(i)).append(") - Error: ")
    440                     .append(mErrorList.get(i).getMessage()).append("; ");
    441             }
    442             throw new AssertionError(buf.toString());
    443         }
    444     }
    445 
    446     public static String bytesToHex(byte[] bytes, int offset, int length) {
    447         if (offset == -1) {
    448             offset = 0;
    449         }
    450 
    451         if (length == -1) {
    452             length = bytes.length;
    453         }
    454 
    455         final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    456         char[] hexChars = new char[length * 3];
    457         int v;
    458         for (int i = 0; i < length; i++) {
    459             v = bytes[offset + i] & 0xFF;
    460             hexChars[i * 3] = hexArray[v >>> 4];
    461             hexChars[i * 3 + 1] = hexArray[v & 0x0F];
    462             hexChars[i * 3 + 2] = ' ';
    463         }
    464         return new String(hexChars);
    465     }
    466 }
    467