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