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