blob: b2f07770fe4a4a7a7b022a2b7eb62edd9b074e13 [file] [log] [blame]
/*
* Copyright (C) 2021 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.android.car.telemetry;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.car.telemetry.IScriptExecutor;
import android.car.telemetry.IScriptExecutorConstants;
import android.car.telemetry.IScriptExecutorListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(JUnit4.class)
public final class ScriptExecutorTest {
private IScriptExecutor mScriptExecutor;
private ScriptExecutor mInstance;
private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
public Bundle mSavedBundle;
public int mErrorType;
public String mMessage;
public String mStackTrace;
public final CountDownLatch mSuccessLatch = new CountDownLatch(1);
public final CountDownLatch mErrorLatch = new CountDownLatch(1);
@Override
public void onScriptFinished(byte[] result) {
}
@Override
public void onSuccess(Bundle stateToPersist) {
mSavedBundle = stateToPersist;
mSuccessLatch.countDown();
}
@Override
public void onError(int errorType, String message, String stackTrace) {
mErrorType = errorType;
mMessage = message;
mStackTrace = stackTrace;
mErrorLatch.countDown();
}
}
private final ScriptExecutorListener mFakeScriptExecutorListener =
new ScriptExecutorListener();
// TODO(b/189241508). Parsing of publishedData is not implemented yet.
// Null is the only accepted input.
private final Bundle mPublishedData = null;
private final Bundle mSavedState = new Bundle();
private static final String LUA_SCRIPT =
"function hello(state)\n"
+ " print(\"Hello World\")\n"
+ "end\n";
private static final String LUA_METHOD = "hello";
private final CountDownLatch mBindLatch = new CountDownLatch(1);
private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
private static final int SCRIPT_SUCCESS_TIMEOUT_SEC = 10;
private static final int SCRIPT_ERROR_TIMEOUT_SEC = 10;
private final ServiceConnection mScriptExecutorConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
mBindLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName className) {
fail("Service unexpectedly disconnected");
}
};
// Helper method to invoke the script and wait for it to complete and return the result.
private void runScriptAndWaitForResult(String script, String function, Bundle previousState)
throws RemoteException {
mScriptExecutor.invokeScript(script, function, mPublishedData, previousState,
mFakeScriptExecutorListener);
try {
if (!mFakeScriptExecutorListener.mSuccessLatch.await(SCRIPT_SUCCESS_TIMEOUT_SEC,
TimeUnit.SECONDS)) {
fail("Failed to get on_success called by the script on time");
}
} catch (InterruptedException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
private void runScriptAndWaitForError(String script, String function) throws RemoteException {
mScriptExecutor.invokeScript(script, function, mPublishedData, new Bundle(),
mFakeScriptExecutorListener);
try {
if (!mFakeScriptExecutorListener.mErrorLatch.await(SCRIPT_ERROR_TIMEOUT_SEC,
TimeUnit.SECONDS)) {
fail("Failed to get on_error called by the script on time");
}
} catch (InterruptedException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
@Before
public void setUp() throws InterruptedException {
mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
Context.BIND_AUTO_CREATE, "scriptexecutor", mContext.getMainExecutor(),
mScriptExecutorConnection);
if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
fail("Failed to bind to ScripExecutor service");
}
}
@Test
public void invokeScript_helloWorld() throws RemoteException {
// Expect to load "hello world" script successfully and push the function.
mScriptExecutor.invokeScript(LUA_SCRIPT, LUA_METHOD, mPublishedData, mSavedState,
mFakeScriptExecutorListener);
// Sleep, otherwise the test case will complete before the script loads
// because invokeScript is non-blocking.
try {
// TODO(b/192285332): Replace sleep logic with waiting for specific callbacks
// to be called once they are implemented. Otherwise, this could be a flaky test.
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
@Test
public void invokeScript_returnsResult() throws RemoteException {
String returnResultScript =
"function hello(state)\n"
+ " result = {hello=\"world\"}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResult(returnResultScript, "hello", mSavedState);
// Expect to get back a bundle with a single string key: string value pair:
// {"hello": "world"}
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("hello")).isEqualTo("world");
}
@Test
public void invokeScript_allSupportedTypes() throws RemoteException {
String script =
"function knows(state)\n"
+ " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResult(script, "knows", mSavedState);
// Expect to get back a bundle with 4 keys, each corresponding to a distinct supported type.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo("hello");
assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
}
@Test
public void invokeScript_skipsUnsupportedTypes() throws RemoteException {
String script =
"function nested(state)\n"
+ " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ " result.nested_table = {x=0, y=0}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResult(script, "nested", mSavedState);
// Bundle does not contain any value under "nested_table" key, because nested tables are
// not supported yet.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("nested_table")).isNull();
}
@Test
public void invokeScript_emptyBundle() throws RemoteException {
String script =
"function empty(state)\n"
+ " result = {}\n"
+ " on_success(result)\n"
+ "end\n";
runScriptAndWaitForResult(script, "empty", mSavedState);
// If a script returns empty table as the result, we get an empty bundle.
assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(0);
}
@Test
public void invokeScript_processPreviousStateAndReturnResult() throws RemoteException {
// Here we verify that the script actually processes provided state from a previous run
// and makes calculation based on that and returns the result.
// TODO(b/189241508): update function signatures.
String script =
"function update(state)\n"
+ " result = {y = state.x+1}\n"
+ " on_success(result)\n"
+ "end\n";
Bundle previousState = new Bundle();
previousState.putInt("x", 1);
runScriptAndWaitForResult(script, "update", previousState);
// Verify that y = 2, because y = x + 1 and x = 1.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("y")).isEqualTo(2);
}
@Test
public void invokeScript_allSupportedTypesWorkRoundTripWithKeyNamesPreserved()
throws RemoteException {
// Here we verify that all supported types in supplied previous state Bundle are interpreted
// by the script as expected.
// TODO(b/189241508): update function signatures.
String script =
"function update_all(state)\n"
+ " result = {}\n"
+ " result.integer = state.integer + 1\n"
+ " result.number = state.number + 0.1\n"
+ " result.boolean = not state.boolean\n"
+ " result.string = state.string .. \"CADABRA\"\n"
+ " on_success(result)\n"
+ "end\n";
Bundle previousState = new Bundle();
previousState.putInt("integer", 1);
previousState.putDouble("number", 0.1);
previousState.putBoolean("boolean", false);
previousState.putString("string", "ABRA");
runScriptAndWaitForResult(script, "update_all", previousState);
// Verify that keys are preserved but the values are modified as expected.
assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(2);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(0.2);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
"ABRACADABRA");
}
@Test
public void invokeScript_scriptCallsOnError() throws RemoteException {
String script =
"function calls_on_error()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(\"one is not equal to two\")\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "calls_on_error");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo("one is not equal to two");
}
@Test
public void invokeScript_tooManyParametersInOnError() throws RemoteException {
String script =
"function too_many_params_in_on_error()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(\"param1\", \"param2\")\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "too_many_params_in_on_error");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_error can push only a single string parameter from Lua");
}
@Test
public void invokeScript_onErrorOnlyAcceptsString() throws RemoteException {
String script =
"function only_string()\n"
+ " if 1 ~= 2 then\n"
+ " on_error(false)\n"
+ " return\n"
+ " end\n"
+ "end\n";
runScriptAndWaitForError(script, "only_string");
assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
"on_error can push only a single string parameter from Lua");
}
}