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      */
SensorCtsHelper()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      */
getPercentileValue( Collection<TValue> collection, float lowPecentile, float highPercentile)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      */
getMean(Collection<TValue> collection)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      */
getVariance(Collection<TValue> collection)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      */
getSamplingPeriodNs(List<TestSensorEvent> collection)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      */
getStandardDeviation( Collection<TValue> collection)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      */
getFrequency(TValue period, TimeUnit unit)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      */
getPeriod(TValue frequency, TimeUnit unit)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      */
clamp(TValue val, TValue min, TValue max)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      */
getMagnitude(float[] values)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      */
sleep(long duration, TimeUnit timeUnit)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      */
formatAssertionMessage(String label, TestSensorEnvironment environment)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      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String format, Object ... params)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      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String extras)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      */
formatFloatArray(float[] array)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      */
getSensorTestDataDirectory()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      */
getSensorTestDataDirectory(String subdirectory)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      */
sanitizeStringForFileName(String value)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      */
createDirectoryStructure(File directoryStructure)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      */
validateCollection(Collection<T> collection)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 
getUnitsForSensor(Sensor sensor)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 
sensorTypeShortString(int type)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 
TestResultCollector()355         public TestResultCollector() {
356             this("Test");
357         }
358 
TestResultCollector(String test)359         public TestResultCollector(String test) {
360             this(test, "SensorCtsTest");
361         }
362 
TestResultCollector(String test, String tag)363         public TestResultCollector(String test, String tag) {
364             mTestName = test;
365             mTag = tag;
366         }
367 
perform(Runnable r)368         public void perform(Runnable r) {
369             perform(r, "");
370         }
371 
perform(Runnable r, String s)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 
judge()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 
bytesToHex(byte[] bytes, int offset, int length)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