blob: 0cfe924d2e400e521f1c3b5d399b43adec96abde [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.chre.test.chqts;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubClientCallback;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppBinary;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.util.Log;
import com.google.android.utils.chre.ChreTestUtil;
import org.junit.Assert;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* A class that can execute the CHQTS "general" tests. Nanoapps using this "general" test framework
* have the name "general_test".
*
* A test successfully passes in one of two ways:
* - MessageType.SUCCESS received from the Nanoapp by this infrastructure.
* - A call to ContextHubGeneralTestExecutor.pass() by the test code.
*
* NOTE: A test must extend this class and define the handleNanoappMessage() function to handle
* specific messages for the test.
*
* TODO: Refactor this class to be able to be invoked for < P builds.
*/
public abstract class ContextHubGeneralTestExecutor extends ContextHubClientCallback {
public static final String TAG = "ContextHubGeneralTestExecutor";
private final List<GeneralTestNanoApp> mGeneralTestNanoAppList;
private final Set<Long> mNanoAppIdSet;
private ContextHubClient mContextHubClient;
private final ContextHubManager mContextHubManager;
private final ContextHubInfo mContextHubInfo;
private CountDownLatch mCountDownLatch;
private boolean mInitialized = false;
private AtomicReference<String> mErrorString = new AtomicReference<>(null);
private long mThreadId;
/**
* A container class to describe a general_test nanoapp.
*/
public static class GeneralTestNanoApp {
private final NanoAppBinary mNanoAppBinary;
private final ContextHubTestConstants.TestNames mTestName;
// Set to false if the nanoapp should not be loaded at init. An example of why this may be
// needed are for nanoapps that are loaded in the middle of the test execution, but still
// needs to be included in this test executor (e.g. deliver messages from it).
private final boolean mLoadAtInit;
public GeneralTestNanoApp(NanoAppBinary nanoAppBinary,
ContextHubTestConstants.TestNames testName) {
mTestName = testName;
mNanoAppBinary = nanoAppBinary;
mLoadAtInit = true;
}
public GeneralTestNanoApp(NanoAppBinary nanoAppBinary,
ContextHubTestConstants.TestNames testName, boolean loadAtInit) {
mTestName = testName;
mNanoAppBinary = nanoAppBinary;
mLoadAtInit = loadAtInit;
}
public NanoAppBinary getNanoAppBinary() {
return mNanoAppBinary;
}
public ContextHubTestConstants.TestNames getTestName() {
return mTestName;
}
public boolean loadAtInit() {
return mLoadAtInit;
}
}
/**
* Note that this constructor accepts multiple general_test nanoapps to test.
*/
public ContextHubGeneralTestExecutor(ContextHubManager manager, ContextHubInfo info,
GeneralTestNanoApp... tests) {
mContextHubManager = manager;
mContextHubInfo = info;
mGeneralTestNanoAppList = new ArrayList<>(Arrays.asList(tests));
mNanoAppIdSet = new HashSet<>();
for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
mNanoAppIdSet.add(test.getNanoAppBinary().getNanoAppId());
}
}
@Override
public void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
if (mNanoAppIdSet.contains(message.getNanoAppId())) {
NanoAppMessage realMessage = hackMessageFromNanoApp(message);
int messageType = realMessage.getMessageType();
ContextHubTestConstants.MessageType messageEnum =
ContextHubTestConstants.MessageType.fromInt(messageType, "");
byte[] data = realMessage.getMessageBody();
switch (messageEnum) {
case INVALID_MESSAGE_TYPE: // fall-through
case FAILURE: // fall-through
case INTERNAL_FAILURE:
// These are univeral failure conditions for all tests.
// If they have data, it's expected to be an ASCII string.
String errorString = new String(data, Charset.forName("US-ASCII"));
fail(errorString);
break;
case SKIPPED:
// TODO: Use junit Assume
String reason = new String(data, Charset.forName("US-ASCII"));
Log.w(TAG, "SKIPPED " + ":" + reason);
pass();
break;
case SUCCESS:
// This is a universal success for the test. We ignore
// 'data'.
pass();
break;
default:
handleMessageFromNanoApp(message.getNanoAppId(), messageEnum, data);
}
}
}
/**
* Should be invoked before run() is invoked to set up the test, e.g. in a @Before method.
*/
public void init() {
Assert.assertFalse("init() must not be invoked when already initialized", mInitialized);
mContextHubClient = mContextHubManager.createClient(mContextHubInfo, this);
Assert.assertTrue(mContextHubClient != null);
unloadAllNanoApps();
for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
if (test.loadAtInit()) {
ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo,
test.getNanoAppBinary());
}
}
mErrorString.set(null);
mInitialized = true;
}
/**
* Run the test.
*/
public void run(long timeoutSeconds) {
mThreadId = Thread.currentThread().getId();
mCountDownLatch = new CountDownLatch(1);
for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
if (test.loadAtInit()) {
sendMessageToNanoAppOrFail(test.getNanoAppBinary().getNanoAppId(),
test.getTestName().asInt(), new byte[0] /* data */);
}
}
boolean success = false;
try {
success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Assert.fail(e.getMessage());
}
Assert.assertTrue("Test timed out", success);
}
/**
* Invoke to indicate that the test has passed.
*/
public void pass() {
mCountDownLatch.countDown();
}
/**
* Cleans up the test, should be invoked in e.g. @After method.
*/
public void deinit() {
Assert.assertTrue("deinit() must be invoked after init()", mInitialized);
// TODO: If the nanoapp aborted (i.e. test failed), wait for CHRE reset or nanoapp abort
// callback, and otherwise assert unload success.
for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
mContextHubManager.unloadNanoApp(mContextHubInfo,
test.getNanoAppBinary().getNanoAppId());
}
mContextHubClient.close();
mContextHubClient = null;
mInitialized = false;
if (mErrorString.get() != null) {
Assert.fail(mErrorString.get());
}
}
/**
* Sends a message to the test nanoapp.
*
* @param nanoAppId The 64-bit ID of the nanoapp to send the message to.
* @param type The message type.
* @param data The message payload.
*/
protected void sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data) {
NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(
nanoAppId, type, data);
int result = mContextHubClient.sendMessageToNanoApp(hackMessageToNanoApp(message));
if (result != ContextHubTransaction.RESULT_SUCCESS) {
fail("Failed to send message: result = " + result);
}
}
/**
* @param errorMessage The error message to display
*/
protected void fail(String errorMessage) {
assertTrue(errorMessage, false /* condition */);
}
/**
* Semantics the same as Assert.assertEquals.
*/
protected <T> void assertEquals(String errorMessage, T expected, T actual) {
if (Thread.currentThread().getId() == mThreadId) {
Assert.assertEquals(errorMessage, expected, actual);
} else if ((expected == null && actual != null) || (expected != null && !expected.equals(
actual))) {
mErrorString.set(errorMessage + ": " + expected + " != " + actual);
mCountDownLatch.countDown();
}
}
/**
* Semantics the same as Assert.assertTrue.
*/
protected void assertTrue(String errorMessage, boolean condition) {
if (Thread.currentThread().getId() == mThreadId) {
Assert.assertTrue(errorMessage, condition);
} else if (!condition) {
mErrorString.set(errorMessage);
mCountDownLatch.countDown();
}
}
/**
* Semantics are the same as Assert.assertFalse.
*/
protected void assertFalse(String errorMessage, boolean condition) {
assertTrue(errorMessage, !condition);
}
protected ContextHubManager getContextHubManager() {
return mContextHubManager;
}
protected ContextHubInfo getContextHubInfo() {
return mContextHubInfo;
}
/**
* Handles a message specific for a test.
*
* @param nanoAppId The 64-bit ID of the nanoapp sending the message.
* @param type The message type.
* @param data The message body.
*/
protected abstract void handleMessageFromNanoApp(
long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data);
private void unloadAllNanoApps() {
List<NanoAppState> stateList =
ChreTestUtil.queryNanoAppsAssertSuccess(mContextHubManager, mContextHubInfo);
for (NanoAppState state : stateList) {
ChreTestUtil.unloadNanoAppAssertSuccess(
mContextHubManager, mContextHubInfo, state.getNanoAppId());
}
}
// TODO: Remove this hack
protected NanoAppMessage hackMessageToNanoApp(NanoAppMessage message) {
// For NYC, we are not able to assume that the messageType correctly
// makes it to the nanoapp. So we put it, in little endian, as the
// first four bytes of the message.
byte[] origData = message.getMessageBody();
ByteBuffer newData = ByteBuffer.allocate(4 + origData.length);
newData.order(ByteOrder.LITTLE_ENDIAN);
newData.putInt(message.getMessageType());
newData.put(origData);
return NanoAppMessage.createMessageToNanoApp(
message.getNanoAppId(), message.getMessageType(), newData.array());
}
// TODO: Remove this hack
protected NanoAppMessage hackMessageFromNanoApp(NanoAppMessage message) {
// For now, our nanohub HAL and JNI code end up not sending across the
// message type of the user correctly. So our testing protocol hacks
// around this by putting the message type in the first four bytes of
// the data payload, in little endian.
ByteBuffer origData = ByteBuffer.wrap(message.getMessageBody());
origData.order(ByteOrder.LITTLE_ENDIAN);
int newMessageType = origData.getInt();
// The new data is the remainder of this array (which could be empty).
byte[] newData = new byte[origData.remaining()];
origData.get(newData);
return NanoAppMessage.createMessageFromNanoApp(
message.getNanoAppId(), newMessageType, newData,
message.isBroadcastMessage());
}
}