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