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 202 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 203 if (test.loadAtInit()) { 204 ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo, 205 test.getNanoAppBinary()); 206 } 207 } 208 209 mErrorString.set(null); 210 } 211 212 /** 213 * Run the test. 214 */ run(long timeoutSeconds)215 public void run(long timeoutSeconds) { 216 mThreadId = Thread.currentThread().getId(); 217 218 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 219 if (test.loadAtInit() && test.sendStartMessage()) { 220 sendMessageToNanoAppOrFail(test.getNanoAppBinary().getNanoAppId(), 221 test.getTestName().asInt(), new byte[0] /* data */); 222 } 223 } 224 225 boolean success = false; 226 try { 227 success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); 228 } catch (InterruptedException e) { 229 Assert.fail(e.getMessage()); 230 } 231 232 Assert.assertTrue("Test timed out", success); 233 } 234 235 /** 236 * Invoke to indicate that the test has passed. 237 */ pass()238 public void pass() { 239 mCountDownLatch.countDown(); 240 } 241 242 /** 243 * Cleans up the test, should be invoked in e.g. @After method. 244 */ deinit()245 public void deinit() { 246 Assert.assertTrue("deinit() must be invoked after init()", mInitialized); 247 248 // TODO: If the nanoapp aborted (i.e. test failed), wait for CHRE reset or nanoapp abort 249 // callback, and otherwise assert unload success. 250 for (GeneralTestNanoApp test : mGeneralTestNanoAppList) { 251 ChreTestUtil.unloadNanoApp(mContextHubManager, mContextHubInfo, 252 test.getNanoAppBinary().getNanoAppId()); 253 } 254 255 mContextHubClient.close(); 256 mContextHubClient = null; 257 258 mInitialized = false; 259 260 if (mErrorString.get() != null) { 261 Assert.fail(mErrorString.get()); 262 } 263 } 264 265 /** 266 * Sends a message to the test nanoapp. 267 * 268 * @param nanoAppId The 64-bit ID of the nanoapp to send the message to. 269 * @param type The message type. 270 * @param data The message payload. 271 */ sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data)272 protected void sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data) { 273 NanoAppMessage message = NanoAppMessage.createMessageToNanoApp( 274 nanoAppId, type, data); 275 276 int result = mContextHubClient.sendMessageToNanoApp(hackMessageToNanoApp(message)); 277 if (result != ContextHubTransaction.RESULT_SUCCESS) { 278 fail("Failed to send message: result = " + result); 279 } 280 } 281 282 /** 283 * @param errorMessage The error message to display 284 */ fail(String errorMessage)285 protected void fail(String errorMessage) { 286 assertTrue(errorMessage, false /* condition */); 287 } 288 289 /** 290 * Semantics the same as Assert.assertEquals. 291 */ assertEquals(String errorMessage, T expected, T actual)292 protected <T> void assertEquals(String errorMessage, T expected, T actual) { 293 if (Thread.currentThread().getId() == mThreadId) { 294 Assert.assertEquals(errorMessage, expected, actual); 295 } else if ((expected == null && actual != null) || (expected != null && !expected.equals( 296 actual))) { 297 mErrorString.set(errorMessage + ": " + expected + " != " + actual); 298 mCountDownLatch.countDown(); 299 } 300 } 301 302 /** 303 * Semantics the same as Assert.assertTrue. 304 */ assertTrue(String errorMessage, boolean condition)305 protected void assertTrue(String errorMessage, boolean condition) { 306 if (Thread.currentThread().getId() == mThreadId) { 307 Assert.assertTrue(errorMessage, condition); 308 } else if (!condition) { 309 mErrorString.set(errorMessage); 310 mCountDownLatch.countDown(); 311 } 312 } 313 314 /** 315 * Semantics are the same as Assert.assertFalse. 316 */ assertFalse(String errorMessage, boolean condition)317 protected void assertFalse(String errorMessage, boolean condition) { 318 assertTrue(errorMessage, !condition); 319 } 320 getContextHubManager()321 protected ContextHubManager getContextHubManager() { 322 return mContextHubManager; 323 } 324 getContextHubInfo()325 protected ContextHubInfo getContextHubInfo() { 326 return mContextHubInfo; 327 } 328 329 /** 330 * Handles a message specific for a test. 331 * 332 * @param nanoAppId The 64-bit ID of the nanoapp sending the message. 333 * @param type The message type. 334 * @param data The message body. 335 */ handleMessageFromNanoApp( long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data)336 protected abstract void handleMessageFromNanoApp( 337 long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data); 338 339 // TODO: Remove this hack hackMessageToNanoApp(NanoAppMessage message)340 protected NanoAppMessage hackMessageToNanoApp(NanoAppMessage message) { 341 // For NYC, we are not able to assume that the messageType correctly 342 // makes it to the nanoapp. So we put it, in little endian, as the 343 // first four bytes of the message. 344 byte[] origData = message.getMessageBody(); 345 ByteBuffer newData = ByteBuffer.allocate(4 + origData.length); 346 newData.order(ByteOrder.LITTLE_ENDIAN); 347 newData.putInt(message.getMessageType()); 348 newData.put(origData); 349 return NanoAppMessage.createMessageToNanoApp( 350 message.getNanoAppId(), message.getMessageType(), newData.array()); 351 } 352 353 // TODO: Remove this hack hackMessageFromNanoApp(NanoAppMessage message)354 protected NanoAppMessage hackMessageFromNanoApp(NanoAppMessage message) { 355 // For now, our nanohub HAL and JNI code end up not sending across the 356 // message type of the user correctly. So our testing protocol hacks 357 // around this by putting the message type in the first four bytes of 358 // the data payload, in little endian. 359 ByteBuffer origData = ByteBuffer.wrap(message.getMessageBody()); 360 origData.order(ByteOrder.LITTLE_ENDIAN); 361 int newMessageType = origData.getInt(); 362 // The new data is the remainder of this array (which could be empty). 363 byte[] newData = new byte[origData.remaining()]; 364 origData.get(newData); 365 return NanoAppMessage.createMessageFromNanoApp( 366 message.getNanoAppId(), newMessageType, newData, 367 message.isBroadcastMessage()); 368 } 369 } 370