1 /* 2 * Copyright (C) 2020 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 com.google.android.chre.test.chqts; 17 18 import android.hardware.location.ContextHubClient; 19 import android.hardware.location.ContextHubClientCallback; 20 import android.hardware.location.ContextHubInfo; 21 import android.hardware.location.ContextHubManager; 22 import android.hardware.location.ContextHubTransaction; 23 import android.hardware.location.NanoAppBinary; 24 import android.hardware.location.NanoAppMessage; 25 import android.util.Log; 26 27 import com.google.android.utils.chre.ChreTestUtil; 28 29 import org.junit.Assert; 30 31 import java.nio.ByteBuffer; 32 import java.nio.ByteOrder; 33 import java.nio.charset.Charset; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Set; 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.TimeUnit; 41 import java.util.concurrent.atomic.AtomicReference; 42 43 /** 44 * A class that can execute the CHQTS "general" tests. Nanoapps using this "general" test framework 45 * have the name "general_test". 46 * 47 * A test successfully passes in one of two ways: 48 * - MessageType.SUCCESS received from the Nanoapp by this infrastructure. 49 * - A call to ContextHubGeneralTestExecutor.pass() by the test code. 50 * 51 * NOTE: A test must extend this class and define the handleNanoappMessage() function to handle 52 * specific messages for the test. 53 * 54 * TODO: Refactor this class to be able to be invoked for < P builds. 55 */ 56 public abstract class ContextHubGeneralTestExecutor extends ContextHubClientCallback { 57 public static final String TAG = "ContextHubGeneralTestExecutor"; 58 59 private final List<GeneralTestNanoApp> mGeneralTestNanoAppList; 60 61 private final Set<Long> mNanoAppIdSet; 62 63 private ContextHubClient mContextHubClient; 64 65 private final ContextHubManager mContextHubManager; 66 67 private final ContextHubInfo mContextHubInfo; 68 69 private CountDownLatch mCountDownLatch; 70 71 private boolean mInitialized = false; 72 73 private AtomicReference<String> mErrorString = new AtomicReference<>(null); 74 75 private long mThreadId; 76 77 /** 78 * A container class to describe a general_test nanoapp. 79 */ 80 public static class GeneralTestNanoApp { 81 private final NanoAppBinary mNanoAppBinary; 82 private final ContextHubTestConstants.TestNames mTestName; 83 84 // Set to false if the nanoapp should not be loaded at init. An example of why this may be 85 // needed are for nanoapps that are loaded in the middle of the test execution, but still 86 // needs to be included in this test executor (e.g. deliver messages from it). 87 private final boolean mLoadAtInit; 88 89 // Set to false if the nanoapp should not send a start message at init. An example of where 90 // this is not needed is for test nanoapps that use the general_test protocol, but do not 91 // require a start message (e.g. starts on load like the busy_startup nanoapp). 92 private final boolean mSendStartMessage; 93 GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName)94 public GeneralTestNanoApp(NanoAppBinary nanoAppBinary, 95 ContextHubTestConstants.TestNames testName) { 96 mTestName = testName; 97 mNanoAppBinary = nanoAppBinary; 98 mLoadAtInit = true; 99 mSendStartMessage = true; 100 } 101 GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName, boolean loadAtInit)102 public GeneralTestNanoApp(NanoAppBinary nanoAppBinary, 103 ContextHubTestConstants.TestNames testName, boolean loadAtInit) { 104 mTestName = testName; 105 mNanoAppBinary = nanoAppBinary; 106 mLoadAtInit = loadAtInit; 107 mSendStartMessage = true; 108 } 109 GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName, boolean loadAtInit, boolean sendStartMessage)110 public GeneralTestNanoApp(NanoAppBinary nanoAppBinary, 111 ContextHubTestConstants.TestNames testName, 112 boolean loadAtInit, boolean sendStartMessage) { 113 mTestName = testName; 114 mNanoAppBinary = nanoAppBinary; 115 mLoadAtInit = loadAtInit; 116 mSendStartMessage = sendStartMessage; 117 } 118 getNanoAppBinary()119 public NanoAppBinary getNanoAppBinary() { 120 return mNanoAppBinary; 121 } 122 getTestName()123 public ContextHubTestConstants.TestNames getTestName() { 124 return mTestName; 125 } 126 loadAtInit()127 public boolean loadAtInit() { 128 return mLoadAtInit; 129 } 130 sendStartMessage()131 public boolean sendStartMessage() { 132 return mSendStartMessage; 133 } 134 } 135 136 /** 137 * Note that this constructor accepts multiple general_test nanoapps to test. 138 */ ContextHubGeneralTestExecutor(ContextHubManager manager, ContextHubInfo info, GeneralTestNanoApp... tests)139 public ContextHubGeneralTestExecutor(ContextHubManager manager, ContextHubInfo info, 140 GeneralTestNanoApp... tests) { 141 mContextHubManager = manager; 142 mContextHubInfo = info; 143 mGeneralTestNanoAppList = new ArrayList<>(Arrays.asList(tests)); 144 mNanoAppIdSet = new HashSet<>(); 145 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 146 mNanoAppIdSet.add(test.getNanoAppBinary().getNanoAppId()); 147 } 148 } 149 150 @Override onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message)151 public void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) { 152 if (mNanoAppIdSet.contains(message.getNanoAppId())) { 153 NanoAppMessage realMessage = hackMessageFromNanoApp(message); 154 155 int messageType = realMessage.getMessageType(); 156 ContextHubTestConstants.MessageType messageEnum = 157 ContextHubTestConstants.MessageType.fromInt(messageType, ""); 158 byte[] data = realMessage.getMessageBody(); 159 160 switch (messageEnum) { 161 case INVALID_MESSAGE_TYPE: // fall-through 162 case FAILURE: // fall-through 163 case INTERNAL_FAILURE: 164 // These are univeral failure conditions for all tests. 165 // If they have data, it's expected to be an ASCII string. 166 String errorString = new String(data, Charset.forName("US-ASCII")); 167 fail(errorString); 168 break; 169 170 case SKIPPED: 171 // TODO: Use junit Assume 172 String reason = new String(data, Charset.forName("US-ASCII")); 173 Log.w(TAG, "SKIPPED " + ":" + reason); 174 pass(); 175 break; 176 177 case SUCCESS: 178 // This is a universal success for the test. We ignore 179 // 'data'. 180 pass(); 181 break; 182 183 default: 184 handleMessageFromNanoApp(message.getNanoAppId(), messageEnum, data); 185 } 186 } 187 } 188 189 /** 190 * Should be invoked before run() is invoked to set up the test, e.g. in a @Before method. 191 */ init()192 public void init() { 193 Assert.assertFalse("init() must not be invoked when already initialized", mInitialized); 194 195 mInitialized = true; 196 197 // Initialize the CountDownLatch before run() since some nanoapps will start on load. 198 mCountDownLatch = new CountDownLatch(1); 199 200 mContextHubClient = mContextHubManager.createClient(mContextHubInfo, this); 201 Assert.assertTrue(mContextHubClient != null); 202 203 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 204 if (test.loadAtInit()) { 205 ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo, 206 test.getNanoAppBinary()); 207 } 208 } 209 210 mErrorString.set(null); 211 } 212 213 /** 214 * Run the test. 215 */ run(long timeoutSeconds)216 public void run(long timeoutSeconds) { 217 mThreadId = Thread.currentThread().getId(); 218 219 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 220 if (test.loadAtInit() && test.sendStartMessage()) { 221 sendMessageToNanoAppOrFail(test.getNanoAppBinary().getNanoAppId(), 222 test.getTestName().asInt(), new byte[0] /* data */); 223 } 224 } 225 226 boolean success = false; 227 try { 228 success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); 229 } catch (InterruptedException e) { 230 Assert.fail(e.getMessage()); 231 } 232 233 Assert.assertTrue("Test timed out", success); 234 } 235 236 /** 237 * Invoke to indicate that the test has passed. 238 */ pass()239 public void pass() { 240 mCountDownLatch.countDown(); 241 } 242 243 /** 244 * Cleans up the test, should be invoked in e.g. @After method. 245 */ deinit()246 public void deinit() { 247 Assert.assertTrue("deinit() must be invoked after init()", mInitialized); 248 249 // TODO: If the nanoapp aborted (i.e. test failed), wait for CHRE reset or nanoapp abort 250 // callback, and otherwise assert unload success. 251 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 252 ChreTestUtil.unloadNanoApp(mContextHubManager, mContextHubInfo, 253 test.getNanoAppBinary().getNanoAppId()); 254 } 255 256 mContextHubClient.close(); 257 mContextHubClient = null; 258 259 mInitialized = false; 260 261 if (mErrorString.get() != null) { 262 Assert.fail(mErrorString.get()); 263 } 264 } 265 266 /** 267 * Sends a message to the test nanoapp. 268 * 269 * @param nanoAppId The 64-bit ID of the nanoapp to send the message to. 270 * @param type The message type. 271 * @param data The message payload. 272 */ sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data)273 protected void sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data) { 274 NanoAppMessage message = NanoAppMessage.createMessageToNanoApp( 275 nanoAppId, type, data); 276 277 int result = mContextHubClient.sendMessageToNanoApp(hackMessageToNanoApp(message)); 278 if (result != ContextHubTransaction.RESULT_SUCCESS) { 279 fail("Failed to send message: result = " + result); 280 } 281 } 282 283 /** 284 * @param errorMessage The error message to display 285 */ fail(String errorMessage)286 protected void fail(String errorMessage) { 287 assertTrue(errorMessage, false /* condition */); 288 } 289 290 /** 291 * Semantics the same as Assert.assertEquals. 292 */ assertEquals(String errorMessage, T expected, T actual)293 protected <T> void assertEquals(String errorMessage, T expected, T actual) { 294 if (Thread.currentThread().getId() == mThreadId) { 295 Assert.assertEquals(errorMessage, expected, actual); 296 } else if ((expected == null && actual != null) || (expected != null && !expected.equals( 297 actual))) { 298 mErrorString.set(errorMessage + ": " + expected + " != " + actual); 299 mCountDownLatch.countDown(); 300 } 301 } 302 303 /** 304 * Semantics the same as Assert.assertTrue. 305 */ assertTrue(String errorMessage, boolean condition)306 protected void assertTrue(String errorMessage, boolean condition) { 307 if (Thread.currentThread().getId() == mThreadId) { 308 Assert.assertTrue(errorMessage, condition); 309 } else if (!condition) { 310 mErrorString.set(errorMessage); 311 mCountDownLatch.countDown(); 312 } 313 } 314 315 /** 316 * Semantics are the same as Assert.assertFalse. 317 */ assertFalse(String errorMessage, boolean condition)318 protected void assertFalse(String errorMessage, boolean condition) { 319 assertTrue(errorMessage, !condition); 320 } 321 getContextHubManager()322 protected ContextHubManager getContextHubManager() { 323 return mContextHubManager; 324 } 325 getContextHubInfo()326 protected ContextHubInfo getContextHubInfo() { 327 return mContextHubInfo; 328 } 329 330 /** 331 * Handles a message specific for a test. 332 * 333 * @param nanoAppId The 64-bit ID of the nanoapp sending the message. 334 * @param type The message type. 335 * @param data The message body. 336 */ handleMessageFromNanoApp( long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data)337 protected abstract void handleMessageFromNanoApp( 338 long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data); 339 340 // TODO: Remove this hack hackMessageToNanoApp(NanoAppMessage message)341 protected NanoAppMessage hackMessageToNanoApp(NanoAppMessage message) { 342 // For NYC, we are not able to assume that the messageType correctly 343 // makes it to the nanoapp. So we put it, in little endian, as the 344 // first four bytes of the message. 345 byte[] origData = message.getMessageBody(); 346 ByteBuffer newData = ByteBuffer.allocate(4 + origData.length); 347 newData.order(ByteOrder.LITTLE_ENDIAN); 348 newData.putInt(message.getMessageType()); 349 newData.put(origData); 350 return NanoAppMessage.createMessageToNanoApp( 351 message.getNanoAppId(), message.getMessageType(), newData.array()); 352 } 353 354 // TODO: Remove this hack hackMessageFromNanoApp(NanoAppMessage message)355 protected NanoAppMessage hackMessageFromNanoApp(NanoAppMessage message) { 356 // For now, our nanohub HAL and JNI code end up not sending across the 357 // message type of the user correctly. So our testing protocol hacks 358 // around this by putting the message type in the first four bytes of 359 // the data payload, in little endian. 360 ByteBuffer origData = ByteBuffer.wrap(message.getMessageBody()); 361 origData.order(ByteOrder.LITTLE_ENDIAN); 362 int newMessageType = origData.getInt(); 363 // The new data is the remainder of this array (which could be empty). 364 byte[] newData = new byte[origData.remaining()]; 365 origData.get(newData); 366 return NanoAppMessage.createMessageFromNanoApp( 367 message.getNanoAppId(), newMessageType, newData, 368 message.isBroadcastMessage()); 369 } 370 } 371