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